Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.47.0-wmf.21
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
Grec ancien/Lexique
0
29558
772930
772491
2026-09-23T13:22:48Z
~2026-50625-01
124585
772930
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, matelas.<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)''' : Smenkhkarê.<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>
'''στρωμνή, -ῆς (nom commun) (f)''' : 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>
51sgtuw026f9fjc1h3mzpqi8t2zzq0n
Fonctionnement d'un ordinateur
0
65764
772931
772105
2026-09-23T17:03:17Z
Mewtow
31375
/* Liens divers */
772931
wikitext
text/x-wiki
{{Page de garde|image=Personal computer, exploded 6.svg|description=
Dans ce cours, vous allez apprendre quels sont les composants d'un ordinateur et comment ceux-ci fonctionnent. Ce cours commencera par des choses simples, comme le binaire, pour arriver progressivement jusqu'au fonctionnement des derniers processeurs, en passant par plein de choses passionnantes comme l'assembleur, les mémoires caches, etc. Ce cours est accessible à n'importe qui, sans prérequis, mais se veut cependant assez complet et riche en informations.
|avancement=Terminé
|cdu=
* {{CDU item|6/68/681|681.3/681.31}}
|versions=
{{version imprimable|page=Fonctionnement d'un ordinateur/Version imprimable 2}}
{{version PDF}}
}}
__NOTOC__
: '''L'auteur du livre tient à remercier vivement Dominus Carnufex et Vayel, qui se sont chargés de la relecture et de la correction orthographique d'une ancienne version de ce cours. Merci à eux pour le travail titanesque qui a été fourni.'''
{{/Sommaire}}
==Liens vers ressources externes==
===Sur wikilivres===
* [[Les cartes graphiques|Les cartes graphiques]], un livre du même auteur qui aborde spécifiquement l'architecture des cartes vidéo.
* [[Monter_un_PC|Monter un PC]], pour une approche bien plus pratique et utile.
* [[Les systèmes d'exploitation|Les systèmes d'exploitation]], un cours qui explique l'interaction entre systèmes d'exploitation et matériel. Le sujet (les systèmes d'exploitation) est si lié au matériel informatique qu'il peut être vu comme un prolongement du présent cours, ou tout du moins comme un très bon complément.
* [[Électronique numérique : logique|Électronique numérique : logique]], un cours d'électronique numérique, disponible sur Wikilivre. Un excellent complément pour les premiers chapitres du cours.
===Liens divers===
*[http://bitsavers.informatik.uni-stuttgart.de/ Bitsavers] : archive des documentations techniques de la plupart du matériel ancien. Tout y est , que ce soit les tout premiers ''mainframes'' de la taille d'une pièce d'appartement aux CPU/GPU des années 2000.
* [http://users.ece.cmu.edu/~koopman/stack_computers/index.html Stack Computers: the new wave] : un livre sur les architectures à pile.
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems] : un livre sur les architectures à capacités.
* [http://www.analogmuseum.org/english/ Museum of analog computing]. Ce site en anglais parle d'un sujet qui n'a pas été abordé dans ce livre : les calculateurs analogiques.
* [https://www.icsa.inf.ed.ac.uk/research/groups/hase/models/ Des simulateurs pour des architectures réelles ou fictives.]
{{AutoCat}}
[[Catégorie:Informatique]]
[[Catégorie:Électronique]]
{{Nouveau livre|}}
ta7y1nbrvi6bipi5kpygs9mip2v0jfo
772939
772931
2026-09-23T19:09:20Z
DavidL
1746
772939
wikitext
text/x-wiki
{{Page de garde|image=Personal computer, exploded 6.svg|description=
Dans ce cours, vous allez apprendre quels sont les composants d'un ordinateur et comment ceux-ci fonctionnent. Ce cours commencera par des choses simples, comme le binaire, pour arriver progressivement jusqu'au fonctionnement des derniers processeurs, en passant par plein de choses passionnantes comme l'assembleur, les mémoires caches, etc. Ce cours est accessible à n'importe qui, sans prérequis, mais se veut cependant assez complet et riche en informations.
|avancement=Terminé
|cdu=
* {{CDU item|6/68/681|681.3/681.31}}
|versions=
{{version imprimable|page=Fonctionnement d'un ordinateur/Version imprimable 2}}
{{version PDF}}
}}
__NOTOC__
: '''L'auteur du livre tient à remercier vivement Dominus Carnufex et Vayel, qui se sont chargés de la relecture et de la correction orthographique d'une ancienne version de ce cours. Merci à eux pour le travail titanesque qui a été fourni.'''
{{/Sommaire}}
==Liens vers ressources externes==
===Sur wikilivres===
* [[Les cartes graphiques|Les cartes graphiques]], un livre du même auteur qui aborde spécifiquement l'architecture des cartes vidéo.
* [[Monter_un_PC|Monter un PC]], pour une approche bien plus pratique et utile.
* [[Les systèmes d'exploitation|Les systèmes d'exploitation]], un cours qui explique l'interaction entre systèmes d'exploitation et matériel. Le sujet (les systèmes d'exploitation) est si lié au matériel informatique qu'il peut être vu comme un prolongement du présent cours, ou tout du moins comme un très bon complément.
* [[Électronique numérique : logique|Électronique numérique : logique]], un cours d'électronique numérique, disponible sur Wikilivre. Un excellent complément pour les premiers chapitres du cours.
===Liens divers===
*[http://bitsavers.informatik.uni-stuttgart.de/ Bitsavers] : archive des documentations techniques de la plupart du matériel ancien. Tout y est, que ce soit les tout premiers ''mainframes'' de la taille d'une pièce d'appartement aux CPU/GPU des années 2000.
* [http://users.ece.cmu.edu/~koopman/stack_computers/index.html Stack Computers: the new wave] : un livre sur les architectures à pile.
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems] : un livre sur les architectures à capacités.
* [http://www.analogmuseum.org/english/ Museum of analog computing]. Ce site en anglais parle d'un sujet qui n'a pas été abordé dans ce livre : les calculateurs analogiques.
* [https://www.icsa.inf.ed.ac.uk/research/groups/hase/models/ Des simulateurs pour des architectures réelles ou fictives.]
{{AutoCat}}
[[Catégorie:Informatique]]
[[Catégorie:Électronique]]
{{Nouveau livre|}}
hoyfdbgxacyb4ry6yqvuzlcre90vsjg
Fonctionnement d'un ordinateur/Les processeurs superscalaires
0
65956
772932
772926
2026-09-23T17:10:59Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772932
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. La séparation entre les deux a eu lieu approximativement dans les années 90. Durant cette décennie, les CPU superscalaires sont devenus très variés, au point qu'ils n'ont pas grand chose en commun. Ils ont tellement évolués qu'ils ont quitté le domaine des CPU historiques.
Les ''CPU superscalaires historiques'' avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont l'Intel 960 CA, le PA-
RISC 7100, et quelques autres. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
kcil4841asirxrk28kfg154abzgzj3w
772933
772932
2026-09-23T17:35:14Z
Mewtow
31375
772933
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. le processeur d'Intel était un design destiné à l'embarqué, mais Intel a rapidement intégré la superscalarité dans les CPU commerciaux. A peine 4 ans plus tard, la superscalarité est apparue sur les CPU x86, avec le Pentium. L'entreprise IBM, quant à elle, a poursuit l'élan lancé avec le RS/6000. Elle a produit de nombreux CPU utilisant les jeux d'instruction POWER, PowerPC, et PowerISA (plus récent), tous superscalaires. Détailler plus demanderait de parler des différences entre ISA POWER et PowerPC.
La compétition n'était pas en reste. Par exemple, en 1991, Motorola a sorti le Motorola 88110, qui était un processeur double émission très intéressant à étudier, dont on reparlera plus bas. Mais c'est en 1992 que plusieurs autres entreprises se sont vraiment jetées dans la bataille. DEC, avec l'architectures Alpha 21064, HP avec le CPU PA-RISC 7100, MIPS avec le R1000 et ses successeurs. et les Fujitsu avec le SuperSPARC.
Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. Et la superscalarité a évolué au fur et à mesure. D'une superscalarité assez simple pour les premiers modèles, elle a évolué pour devenir de plus en plus complexe. L'évolution s’est traduit par un élargissement des processeurs. Les premiers modèles étaient à double émission, mais ont progressivement grossit pour passer de 2 à 3 ou 4 voies. Il y a cependant eu quelques retours en arrière pour les CPU d'IBM, avec quelques retours tardifs à la double émission, mais nous en reparlerons.
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Les CPU superscalaires historiques avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
mbcn74h2bx6y2y19tf7pi7c7rahjjos
772934
772933
2026-09-23T17:48:29Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772934
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. le processeur d'Intel était un design destiné à l'embarqué, mais Intel a rapidement intégré la superscalarité dans les CPU commerciaux. A peine 4 ans plus tard, la superscalarité est apparue sur les CPU x86, avec le Pentium. L'entreprise IBM, quant à elle, a poursuit l'élan lancé avec le RS/6000. Elle a produit de nombreux CPU utilisant les jeux d'instruction POWER, PowerPC, et PowerISA (plus récent), tous superscalaires. Détailler plus demanderait de parler des différences entre ISA POWER et PowerPC.
La compétition n'était pas en reste. Par exemple, en 1991, Motorola a sorti le Motorola 88110, qui était un processeur double émission très intéressant à étudier, dont on reparlera plus bas. Mais c'est en 1992 que plusieurs autres entreprises se sont vraiment jetées dans la bataille. DEC, avec l'architectures Alpha 21064, HP avec le CPU PA-RISC 7100, MIPS avec le R1000 et ses successeurs. et les Fujitsu avec le SuperSPARC.
Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. Et la superscalarité a évolué au fur et à mesure. D'une superscalarité assez simple pour les premiers modèles, elle a évolué pour devenir de plus en plus complexe. L'évolution s’est traduit par un élargissement des processeurs. Les premiers modèles étaient à double émission, mais ont progressivement grossit pour passer de 2 à 3 ou 4 voies. Il y a cependant eu quelques retours en arrière pour les CPU d'IBM, avec quelques retours tardifs à la double émission, mais nous en reparlerons.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel !! MIPS !! Motorola !! Alpha
|-
! 1989
| || RS/6000 || || || i960CA ||
|-
! 1990
| || || || || || ||
|-
! 1991
| || || || || || 88110 ||
|-
! 1992
| || RSC || PA 7100 || SuperSPARC || || || 21064
|-
! 1989
| || || || || || ||
|-
! 1989
| || || || || || ||
|-
! 1989
| || || || || || ||
|-
! 1989
| || || || || || ||
|-
! 1989
| || || || || || ||
|-
! 1989
| || || || || || ||
|-
! 1989
| || || || || || ||
|}
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Les CPU superscalaires historiques avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
nvnqe2h204fuiikbhop8lt1l3fqhno9
772935
772934
2026-09-23T17:55:31Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772935
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. le processeur d'Intel était un design destiné à l'embarqué, mais Intel a rapidement intégré la superscalarité dans les CPU commerciaux. A peine 4 ans plus tard, la superscalarité est apparue sur les CPU x86, avec le Pentium. L'entreprise IBM, quant à elle, a poursuit l'élan lancé avec le RS/6000. Elle a produit de nombreux CPU utilisant les jeux d'instruction POWER, PowerPC, et PowerISA (plus récent), tous superscalaires. Détailler plus demanderait de parler des différences entre ISA POWER et PowerPC.
La compétition n'était pas en reste. Par exemple, en 1991, Motorola a sorti le Motorola 88110, qui était un processeur double émission très intéressant à étudier, dont on reparlera plus bas. Mais c'est en 1992 que plusieurs autres entreprises se sont vraiment jetées dans la bataille. DEC, avec l'architectures Alpha 21064, HP avec le CPU PA-RISC 7100, MIPS avec le R1000 et ses successeurs. et les Fujitsu avec le SuperSPARC.
Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. Et la superscalarité a évolué au fur et à mesure. D'une superscalarité assez simple pour les premiers modèles, elle a évolué pour devenir de plus en plus complexe. L'évolution s’est traduit par un élargissement des processeurs. Les premiers modèles étaient à double émission, mais ont progressivement grossit pour passer de 2 à 3 ou 4 voies. Il y a cependant eu quelques retours en arrière pour les CPU d'IBM, avec quelques retours tardifs à la double émission, mais nous en reparlerons.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Les CPU superscalaires historiques avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
djh2xth3vwpdu7lsy6p3gxhzgkv7glq
772936
772935
2026-09-23T17:56:04Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772936
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. le processeur d'Intel était un design destiné à l'embarqué, mais Intel a rapidement intégré la superscalarité dans les CPU commerciaux. A peine 4 ans plus tard, la superscalarité est apparue sur les CPU x86, avec le Pentium. L'entreprise IBM, quant à elle, a poursuit l'élan lancé avec le RS/6000. Elle a produit de nombreux CPU utilisant les jeux d'instruction POWER, PowerPC, et PowerISA (plus récent), tous superscalaires. Détailler plus demanderait de parler des différences entre ISA POWER et PowerPC.
La compétition n'était pas en reste. Par exemple, en 1991, Motorola a sorti le Motorola 88110, qui était un processeur double émission très intéressant à étudier, dont on reparlera plus bas. Mais c'est en 1992 que plusieurs autres entreprises se sont vraiment jetées dans la bataille. DEC, avec l'architectures Alpha 21064, HP avec le CPU PA-RISC 7100, MIPS avec le R1000 et ses successeurs. et les Fujitsu avec le SuperSPARC.
Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. Et la superscalarité a évolué au fur et à mesure. D'une superscalarité assez simple pour les premiers modèles, elle a évolué pour devenir de plus en plus complexe. L'évolution s’est traduit par un élargissement des processeurs. Les premiers modèles étaient à double émission, mais ont progressivement grossit pour passer de 2 à 3 ou 4 voies. Il y a cependant eu quelques retours en arrière pour les CPU d'IBM, avec quelques retours tardifs à la double émission, mais nous en reparlerons.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Les CPU superscalaires historiques avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
f7to7611gmqojzojg1lkkogrw1mqn9m
772938
772936
2026-09-23T18:05:03Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772938
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
D'une superscalarité assez simple pour les premiers modèles, elle a évolué pour devenir de plus en plus complexe. L'évolution s’est traduit par un élargissement des processeurs. Les premiers modèles étaient à double émission, mais ont progressivement grossit pour passer de 2 à 3 ou 4 voies. Il y a cependant eu quelques retours en arrière pour les CPU d'IBM, avec quelques retours tardifs à la double émission, mais nous en reparlerons.
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Les CPU superscalaires historiques avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
6aam3u64bzem8tv19gn30cbwmfjizgz
772940
772938
2026-09-23T19:22:42Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772940
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
L'évolution des CPU superscalaires est assez difficile à décrire, mais il existe une classification assez intéressante, présentée dans le livre "Modern processor design", par John Shen et Mikko Lipasti. Je ne vais cependant pas réutiliser leur classification directement, mais en donner une version simplifiée. Je vais distinguer les '''CPU superscalaires ''historiques''''' des autres. Les CPU superscalaires historiques avaient un budget en transistors très limité, ce qui fait que les contraintes d'appariement étaient très strictes. En conséquence, ils ont l'avantage d'être très simples. De plus, ils permettent d'intégrer quelques concepts qui sont valables sur les processeurs superscalaires non-historiques. Il y avait une certaine continuité dans les CPU historiques, qui ont évolués en trois phases, trois étapes, que j'ai décidé de nommer :
* double émission entière-flottante ;
* exécution parallèle des branchements ;
* émission multiple des opérations entières.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
dea7af5e3679wxhj1zyyroyww1636sz
772941
772940
2026-09-23T19:24:34Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772941
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission parallèle des branchements===
La double émission entière-flottante était l'implémentation la plus simple qui soit. L'étape suivante visait elle aussi à exploiter les unités de calcul déjà présentes dans le processeur. Il y avait plusieurs candidats : le ''barrel shifter'', le multiplieur, l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Avec l''''émission parallèle des branchements''', le processeur émet un branchement en parallèle des autres instructions. Combinée à la double émission entière-flottante, cela permettait d'émettre en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les branchements étant fréquents sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer les premiers CPU POWER et PowerPC (le ''Power 1'', le ''RISC Single Chip'', le ''PowerPC 601'').
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Branchement || Opération flottante
|-
| ALU entière || Unité de branchement || FPU
|}
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, reliée au registre d'état et au séquenceur. Pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter'' Et il en est de même si le processeur utilise des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité de branchement séparée, si elle n'existait pas avant.
[[File:CPU superscalaire historique avec émission parallèle des branchements.png|centre|vignette|upright=1.5|CPU superscalaire historique avec émission parallèle des branchements.]]
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
aafj7tf770e04dz5p2p5asn5lk4ydsf
772942
772941
2026-09-23T19:25:00Z
Mewtow
31375
/* L'émission parallèle des branchements */
772942
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont le PA-
RISC 7100, et quelques autres. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
dndff0c9vauetacn3olftbpdtuulgvw
772943
772942
2026-09-23T19:26:24Z
Mewtow
31375
/* La double émission entière-flottante */
772943
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
==L'évolution historique des processeurs superscalaires==
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
===L'émission multiple des micro-opérations entières===
Le troisième type de CPU superscalaire historique permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Les processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 !
|-
| Opération entière || Opération entière || Opération flottante
|-
| ALU entière || ALU entière || FPU
|}
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rx4jvpu7lb7bkz8xiax9lqfpfhviiy6
772944
772943
2026-09-23T19:36:51Z
Mewtow
31375
/* L'évolution historique des processeurs superscalaires */
772944
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Un exemple simple : la double émission entière-flottante==
L'implémentation la plus simple d'un CPU superscalaire utilise la '''double émission entière-flottante'''. Elle part d'un processeur avec une ALU entière et une FPU flottante, et l'améliore pour en faire un CPU superscalaire, en modifiant les circuits de chargement, décodage, et d'émission. L'idée est très simple : le CPU charge deux instructions par cycle, les décode, puis l'unité d'émission s'occupe de leur émission. L'unité d'émission peut émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre et d'exécuter de nombreuses opérations entières simultanément. Ce qu'on a appelé l''''émission entière multiple''', plus haut. Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières !
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Emission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
45fvtymikegmu43az0lxvydmpmlxij3
772945
772944
2026-09-23T19:40:49Z
Mewtow
31375
/* La duplication des ALU entières */
772945
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Un exemple simple : la double émission entière-flottante==
L'implémentation la plus simple d'un CPU superscalaire utilise la '''double émission entière-flottante'''. Elle part d'un processeur avec une ALU entière et une FPU flottante, et l'améliore pour en faire un CPU superscalaire, en modifiant les circuits de chargement, décodage, et d'émission. L'idée est très simple : le CPU charge deux instructions par cycle, les décode, puis l'unité d'émission s'occupe de leur émission. L'unité d'émission peut émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ege9ly60lx58jscqqeb08agxb51tycj
772946
772945
2026-09-23T19:41:38Z
Mewtow
31375
/* Un exemple simple : la double émission entière-flottante */
772946
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Un exemple simple : la double émission entière-flottante==
L'implémentation la plus simple d'un CPU superscalaire utilise la '''double émission entière-flottante'''. Elle part d'un processeur avec une ALU entière et une FPU flottante, et l'améliore pour en faire un CPU superscalaire. L'unité d'émission peut émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour les accès mémoire, les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour les branchements, la double émission standard les traitait eux aussi comme des opérations entières. Ils étaient exécutés dans l'ALU entière, leur résultat est envoyé au séquenceur pour que celui-ci altère le ''program counter''. Et cela nous amène au second type de CPU superscalaires historique.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tb4dphw3lj0u78ur5qibab68t9du1rr
772947
772946
2026-09-23T19:48:45Z
Mewtow
31375
/* Un exemple simple : la double émission entière-flottante */
772947
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Un exemple simple : la double émission entière-flottante==
L'implémentation la plus simple d'un CPU superscalaire utilise la '''double émission entière-flottante'''. Elle part d'un processeur avec une ALU entière et une FPU flottante, et l'améliore pour en faire un CPU superscalaire. L'unité d'émission peut émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2
|-
| Opération entière || Opération flottante
|-
| ALU entière || FPU
|}
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne permettent pas la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec cependant des restrictions pour les branchements. Le cas des branchements est assez spécial, et tout dépend du processeur. Les premiers CPU superscalaires d'IBM utilisaient la technique du ''branch folding'', ce qui rend leur cas un peu complexe à traiter et leur a valu l’appellation, erronée selon moi, de CPU ''double émission + branch''. Reste le cas des µops mémoire.
Les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
s68lx9q9viyc04ale5gb45laeijx29e
772948
772947
2026-09-23T19:53:07Z
Mewtow
31375
/* Un exemple simple : la double émission entière-flottante */
772948
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Un exemple simple : la double émission entière-flottante==
L'implémentation la plus simple d'un CPU superscalaire utilise la '''double émission entière-flottante'''. Elle part d'un processeur avec une ALU entière et une FPU flottante, et l'améliore pour en faire un CPU superscalaire. L'unité d'émission peut émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
La mise en œuvre utilise assez peu de circuits, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne permettent pas la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec cependant des restrictions pour les branchements. Le cas des branchements est assez spécial, et tout dépend du processeur. Les premiers CPU superscalaires d'IBM utilisaient la technique du ''branch folding'', ce qui rend leur cas un peu complexe à traiter et leur a valu l’appellation, erronée selon moi, de CPU ''double émission + branch''. Reste le cas des µops mémoire.
Les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fww2ceiuh9u51zw4f4usefiekr6c63j
772949
772948
2026-09-23T19:54:48Z
Mewtow
31375
/* Un exemple simple : la double émission entière-flottante */
772949
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Un exemple simple : la double émission entière-flottante==
L'implémentation la plus simple d'un CPU superscalaire utilise la '''double émission entière-flottante'''. Elle part d'un processeur avec une ALU entière et une FPU flottante, et l'améliore pour en faire un CPU superscalaire. L'unité d'émission peut émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
Il faut noter que quelques subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications ?
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne permettent pas la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec cependant des restrictions pour les branchements. Le processeur a donc deux pipelines séparés : un pipeline pour les opérations flottantes, et un second pipeline généraliste.
Le cas des branchements est assez spécial, et tout dépend du processeur. Les premiers CPU superscalaires d'IBM utilisaient la technique du ''branch folding'', ce qui rend leur cas un peu complexe à traiter et leur a valu l’appellation, erronée selon moi, de CPU ''double émission + branch''. Reste le cas des µops mémoire.
Les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
19r0mnkn2lzab186yo48byszgrx7el2
772950
772949
2026-09-23T19:56:48Z
Mewtow
31375
/* Un exemple simple : la double émission entière-flottante */
772950
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Un exemple simple : la double émission entière-flottante==
L'implémentation la plus simple d'un CPU superscalaire utilise la '''double émission entière-flottante'''. Elle requiert juste un processeur avec une ALU entière et une FPU flottante. L'unité d'émission peut émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec cependant des restrictions pour les branchements. Le processeur a donc deux pipelines séparés : un pipeline pour les opérations flottantes, et un second pipeline généraliste.
Le cas des branchements est assez spécial, et tout dépend du processeur. Les premiers CPU superscalaires d'IBM utilisaient la technique du ''branch folding'', ce qui rend leur cas un peu complexe à traiter et leur a valu l’appellation, erronée selon moi, de CPU ''double émission + branch''. Reste le cas des µops mémoire.
Les µops mémoire étaient considérées comme des instructions entières. Nous détaillerons cela plus tard, mais l'idée est qu'il était impossible d'émettre une opération entière en même temps qu'une µops mémoire. C'est soit l'un, soit l'autre, pas les deux. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous. L'avantage est que les calculs d'adresse sont fait dans l'ALU entière, pas besoin d'unité de calcul d'adresse dédiée.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et mine de rien, certains CPU superscalaires historiques étaient dans ce cas. La double émission entière-flottante est un exemple, qu'elle soit ou pas complétée avec l'émission parallèle des branchements. Mais il est cependant possible d'aller un peu plus loin.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pi3bb8x7iz08dlsk9wqmcin9jlgtdwn
772951
772950
2026-09-23T20:09:43Z
Mewtow
31375
772951
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment ils faisiaent, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Les µops mémoire et les branchements sont juste exécutées dans le pipeline entier. Les branchements aussi exécutés dans l'ALU entière. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9f5rcpr7mooi42h32vhiqxqti0ouv2t
772952
772951
2026-09-23T20:11:28Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772952
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment ils faisiaent, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Les µops mémoire et les branchements sont juste exécutées dans le pipeline entier. Les branchements aussi exécutés dans l'ALU entière. Les µops mémoire passent par l'ALU entière, avant d’atterrir dans l'unité mémoire, comme illustré ci-dessous.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Le cas des multiplications est très intéressant. Intuitivement, on se dit que la multiplication est réalisée dans un circuit multiplier entier, séparé de la FPU. Mais les CPU superscalaires historiques étaient des CPU 32 bits, et les multiplications 32 bits étaient traités différemment selon le processeur, certaines faisant les multiplications dans l'ALU, d'autres dans la FPU. Il y a bel et bien eu des cas où Les multiplications passaient dans le pipeline entier, l'émission était de type (INT/MUL) + FLOAT. Le processeur DEC Alpha 21064 était dans ce cas, de même que le MIPS R8000 ou les IBM POWER1 / PowerPC 601. Mais il y a des contre-exemples.
Les autres processeurs utilisaient la FPU pour faire les multiplications entières. Pour rappel, multiplier deux flottants 64 bits implique de multiplier deux mantisses assez longues. La FPU contient pour cela un multiplieur entier, qui peut être utilisé pour les multiplications entières. Il était alors possible d'émettre une multiplication simultanément avec une autre opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
L'exemple précédent nous donne un moyen pour implémenter la superscalarité. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
e798geehnmdjzml542qfc7fn86ftp1e
772953
772952
2026-09-23T20:17:02Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772953
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour certains CPUs, le terme est doublement trompeur, en raison de subtilités concernant les multiplications. Sur les CPU 32 bits, il arrive que les multiplications soient faites dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire des multiplications entières 32 bits. Sur les CPUs faisant cela, il était possible d'émettre une multiplication en même temps qu'une opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, plus soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). C'était le cas du CPU PA-RISC 7100.
L'exemple précédent nous donne un moyen pour implémenter la superscalarité. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est ppossible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
a79skm78jlwk98zgu27t8q21kpj18u6
772954
772953
2026-09-23T20:21:28Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772954
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement a été abordée plus haut, avec l'usage de la FPU pour faire des multiplications entières. Utiliser la FPU pour faire des multiplications entière était fréquent sur les processeurs 32 bits, mais est tombé en désuétude sur les processeurs 64 bits. Et même sur les processeurs 32 bits, ce n'était pas systématiquement utilisé. En tout cas, sur les processeurs superscalaires qui l'utilisaient, cela permettait d'émettre une multiplication en parallèle d'une autre opération entière simple. On perdait la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ls3mlln3s6iwdtnhofv2ymfqjn6xsit
772955
772954
2026-09-23T20:27:35Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772955
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec partitionnement seul, avec une ALU entière et une FPU, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
Une dernière forme de reconditionnement est liée aux branchements. Il est possible d'exécuter les branchements soit dans une unité de branchement séparée, soit dans une unité de calcul dédiée. Reconditionner une ALU entière pour qu'elle fasse des branchement est utile pour faire des économies de circuits, non seulement car cela économise une ALU, mais aussi car cela réduit la largeur du processeur d'une voie.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Émission série et parallèle des branchements]]
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
2nbl9t5xzvjsxk18qr78p0gqkdon706
772956
772955
2026-09-23T20:27:43Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772956
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec partitionnement seul, avec une ALU entière et une FPU, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération dans l'ALU entière. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
5u8rtvcnrg866vlzvn2fhns8p7eo3em
772957
772956
2026-09-23T20:28:49Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772957
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu, en permettant.
Le PowerPC 603 était un processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
it39gzxg3gdrtpsthm3ogox3yd8r86g
772958
772957
2026-09-23T20:43:12Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772958
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, beaucoup de combinaisons de deux instructions n'étaient pas possibles, même pour des combinaisons de deux instructions compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une instruction mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
q4z240awji7chb3m4ui9e23d0kvvwey
772959
772958
2026-09-23T20:44:22Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772959
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus. La microarchitecture du CPU en question est illustrée ci-dessous.
[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
csj1xx96ivt1jb8nomv0br8u51xi6uo
772962
772959
2026-09-23T20:47:26Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772962
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
halq6648kyqfaqgmjqhgowdyyhpxrf6
772963
772962
2026-09-23T20:53:58Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772963
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnellement''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
cagigwmfq4vez4ytmfjkxvsklwubw4p
772964
772963
2026-09-23T20:54:05Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772964
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. Seule exception : la division, qui est le même port que le multiplieur flottant.
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
qh4h0lqelcva2gvewuvj3wy4ku27j1l
772965
772964
2026-09-23T20:55:04Z
Mewtow
31375
/* Le partitionnement intra-unité */
772965
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Pour l'unité mémoire, il n'est pas possible de la partitionner. Cependant, une optimisation permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
Une autre forme de reconditionnement touche la FPU. L'idée est de la reconditionner pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
hv1hvcdharxoyi8yion5tk9pbtndf2c
772966
772965
2026-09-23T20:56:13Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772966
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation.
Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, une version améliorée de l'Intel 960. L'Intel 960 originel n'était pas exactement superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire. Il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, sous conditions. Son pipeline permettait donc les combinaisons INT + INT* (+ BRANCH), INT + MEM (+ BRANCH), MEM + BRANCH, INT + BRANCH.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
asjednn7gzqyj82p7bat0ti7f84uzht
772968
772966
2026-09-23T21:04:13Z
Mewtow
31375
/* Le reconditionnement des unités fonctionnelles */
772968
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l'''émission multiple des micro-opérations entières''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
p5q48fue154ap21n5i73d0vqb0mtxbk
772970
772968
2026-09-23T21:09:48Z
Mewtow
31375
/* La duplication des ALU entières */
772970
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé. Si les multiplications passent par la FPU, il est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU. Le processeur SuperSPARC était dans ce cas là. Il avait deux ALU entière et une FPU, la multiplication était faite dans la FPU, idem pour la division.
Cependant, il est aussi possible de faire les multiplications dans un multiplieur dédié. Les CPU de l'époque utilisaient un seul multiplieur, pas deux. Il n'était donc pas possible d'émettre deux multiplications à la fois. Par contre, on pouvait émettre une multiplication avec une opération entière, et une opération flottante. En conséquence, les combinaisons INT + (INT/MUL) + FPU sont possibles, mais pas la combinaison INT + INT + MUL. Un exemple de processeur de ce type était l' Alpha 21064 de DEC.
Pour les branchements, il sont généralement exécutés dans l'ALU entière, ce qui fait une économie de circuits. Mais on ne peut plus utiliser l'émission parallèle des branchements. Pour un CPU double émission, cela veut dire qu'on peut émettre soit deux opérations entières, soit une opération entière avec un branchement. Les branchements ne viennent pas en plus des autres instructions.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière + branchements
* Barrel shifter
* Unité mémoire
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Branchements
* Décalages et rotations
* µops mémoire.
| Opération flottante
|}
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rlt3o0io2pjq8mec1eel6q3qqjz1uza
772971
772970
2026-09-23T21:13:03Z
Mewtow
31375
/* La duplication des ALU entières */
772971
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===Le cout en performance des contraintes d’appariement===
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
lddcc1mosrow4875cd0ix8igvaon4x0
772973
772971
2026-09-23T21:15:11Z
Mewtow
31375
/* Le cout en performance des contraintes d’appariement */
772973
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
hw9gogv20jonzx2gkm0t4mnxs52c2xa
772975
772973
2026-09-23T21:15:33Z
Mewtow
31375
/* Les contraintes d’appariement */
772975
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Pour résumer, les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, et duplication des unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le conditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens. Les CPUs superscalaires historiques des années 80-90 l'utilisaient, guère plus. Et c'est pareil avec le reconditionnement des unités, les deux allant bien ensemble. Le reconditionnement permet de mieux profiter du partitionnement, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé, mais en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. Avec cependant deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Nous détaillerons ce processeur dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Les CPU des PowerPC G3 et G4 sont un exemple très parlant. Ils avaient 5 unités de calcul : deux ALU entières, un multiplieur, une FPU et une unité mémoire. Et pourtant, ils implémentaient la double émission, pas plus. Deux ports de décodage, cinq ports d'émission. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
oi7sz4g17o611icjod1wdfvjbk4384f
772984
772975
2026-09-23T21:31:02Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772984
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le reconditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des ALU entières est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. La duplication des autres unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens, pour les tout premiers CPU superscalaires. Et encore, beaucoup d'entre eux n'hésitaient pas à dupliquer leurs ALU entières. Le reconditionnement de l'AGU permet de bien mieux profiter du partitionnement seul, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. L'intel Pentium avait deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Quelques CPU superscalaires anciens utilisaient ce combo, dans sa version la plus simple qui soit. C'était des CPU double émission, avec un partitionnement utilisant : deux ALUs entières, un multiplieur entier, une FPU et l'unité mémoire. L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7zxeau8qpmi1q6gjtjycxwpu4ymho3j
772985
772984
2026-09-23T21:37:38Z
Mewtow
31375
/* Le partitionnement des unités fonctionnelles */
772985
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Parmi les CPU superscalaires historiques, trois d'entre eux utilisaient un partitionnement bien spécifique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le reconditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des ALU entières est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. La duplication des autres unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens, pour les tout premiers CPU superscalaires. Et encore, beaucoup d'entre eux n'hésitaient pas à dupliquer leurs ALU entières. Le reconditionnement de l'AGU permet de bien mieux profiter du partitionnement seul, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. L'intel Pentium avait deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Quelques CPU superscalaires anciens utilisaient ce combo, dans sa version la plus simple qui soit. C'était des CPU double émission, avec un partitionnement utilisant : deux ALUs entières, un multiplieur entier, une FPU et l'unité mémoire. L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Seuls les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
46xlcw5yrvckpahlyeuixm7g3v3x3bv
772987
772985
2026-09-23T21:40:10Z
Mewtow
31375
/* Les CPU superscalaires utilisent toutes ces stratégies */
772987
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==Le nombre de voies d'un processeur superscalaire==
Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme !
Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague.
Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toute chose égale par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. A budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des fenêtres d'instructions/ROB et autres.
===Les contraintes d’appariement===
Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc.
En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire.
Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps.
Ces contraintes d’appariement ont un impact sur les performance, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout.
La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement.
Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes :
* INT est un raccourcis pour parler d'une opération entière simple, qu'une ALU entière peut exécuter.
* MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division.
* FLOAT désigne une opération flottante, peut importe laquelle.
* MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire.
* BRANCH désigne une instruction de branchement.
Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière.
J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante.
===L'évolution historique des processeurs superscalaires===
L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais pas commercialisés. L'exemple type est celui de l'ACS-1 d'IBM, dont la conception a été stoppée avant sa production. Les années 1980 ont vu l'arrivée de processeurs à émission multiples, mais qui n'étaient pas vraiment superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. C'était des processeurs VLIW avant l'heure.
Les premiers CPU superscalaires à avoir été commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année là : le RS/6000 d'IBM et l'i960CA d'Intel. La compétition n'était pas en reste, avec le Motorola 88110, l'architectures Alpha 21064, le CPU PA-RISC 7100, MIPS avec le R1000, le SuperSPARC. Tous ont eu des successeurs, qui étaient tous des CPU superscalaires plus ou moins évolués. La timeline précise est décrite dans le tableau ci-dessous.
{|class="wikitable"
|-
! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres
|-
! 1989
| RS/6000 || || || i960CA || ||
|-
! 1990
| || || || || ||
|-
! 1991
| || || || || || Motorola 88110
|-
! 1992
| RSC || PA 7100 || SuperSPARC || || 21064 ||
|-
! 1993
| PPC 601 || || HyperSPARC || Pentium || ||
|-
! 1994
| POWER 2 || PA 7200 || || || 21164 ||
|-
! 1995
| 603/604 || PA 8000 || UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || MIPS R1000
|-
! 1996
| 620 || || || || 21264 ||
|-
! ...
|| ... || ... || ... || ... || ... || ...
|}
La plupart des premiers modèles étaient à double émission, mais ils sont rapidement montés à de la quadruple émission. Exception fait du cas SPARC, qui a démarré directement avec de la quadruple émission, avant de revenir à la double émission, pour revenir à une largeur importante. De plus, si les premiers modèles de chaque marque étaient à exécution dans l'ordre, l'exécution dans le désordre est rapidement apparue, ce qui modifié fondamentalement l'implémentation de la superscalarité. En tout cas, la morale est que même les premiers CPU superscalaires étaient très complexes, et qu'on ne peut pas en faire une description simplifiée, allant de design simples à des designs complexes.
==Les unités de calcul d'un processeur superscalaire==
Étudier les CPU superscalaires historique était une bonne introduction propédeutique, nous allons maintenant passer aux CPU superscalaires en général. Avant de voir comment est conçu un CPU superscalaire dans le détail, nous allons devoir parler de leurs unités de calcul. Et cela nous permettra de généraliser certains concepts vus sur les CPU superscalaires historiques.
Instinctivement, émettre plusieurs µops demande de charger, décoder et exécuter plusieurs instructions à la fois. Pour cela, l'unité de chargement se contente de charger plusieurs instructions consécutives en mémoire RAM. Elles chargent des blocs de 2 instructions pour la double émission, trois pour la triple émission, etc. On pourrait rentrer dans les détails, mais laissons cela à plus tard. Il est plus intéressant de regarder ce qu'il se passe au niveau des ALUs.
Les unités de calcul et Les contraintes d’appariement sont très reliés. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités de calcul doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, il faudrait dupliquer les ALUs en N exemplaires, ce qui aurait un cout en circuit encore plus prohibitif ! En pratique, aucun CPU superscalaire ne fait cela, ou presque. A la place, ils utilisent d'autres techniques : le partitionnement, le reconditionnement et la duplication partielle des ALUs.
===Le partitionnement des unités fonctionnelles===
Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir d'un exemple très simple, l'implémentation la plus simple d'un CPU superscalaire. Elle requiert juste un processeur avec une ALU entière et une FPU flottante, rien de plus.
La '''double émission entière-flottante''' consiste à émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Le processeur a donc deux pipelines : un pipeline entier, et un pipeline flottant. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Le terme double émission ''entière''-flottante est un peu trompeur. Il sous-entend que les µops mémoire ou les branchements ne sont pas concernés par la double émission. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire, vu que les µops mémoire passent par l'ALU entière avant d’atterrir dans l'unité mémoire.
{|
|[[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU.]]
|[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
|}
Et ce qui peut être fait avec la FPU peut être fait avec les autres unités, comme une unité mémoire ou une unité de branchements. Il suffit d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en-dehors de quelques vieux articles académiques.
Pour l'exemple, nous allons prendre un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément.
Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée.
Par contre, c'est suffisant pour implémenter la double émission. Il est possible d'exploiter 4 ou 5 unités de calcul avec la double émission, la double émission n'est pas limitée à deux unités. Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux instructions tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient nombreux à utiliser cette technique.
Parmi les CPU superscalaires historiques, trois d'entre eux utilisaient un partitionnement bien spécifique. Ils étaient des CPU double émission, avec une ALU entière, une unité mémoire et une FPU. Il s'agit de l'HyperSPARC, de l'alpha 21064 et du PowerPC 603.
Par exemple, le processeur alpha 21064 utilisait cette technique. Il utilisait la double émission , avec trois unités : une ALU entière, une FPU, et une unité mémoire. Il avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur.
Le PowerPC 603 était un autre processeur de ce genre. Il intégrait une ALU entière, une FPU et une unité mémoire. Et ces trois unités étaient utilisées pour implémenter une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''.
===Le partitionnement intra-unité===
Plus haut, j'ai pris l'exemple d'un processeur intégrant ALU, FPU, unité mémoire et unité de branchement. Mais dans la réalité, la FPU, l'ALU, l'unité mémoire sont des regroupements de plusieurs unités plus simples. Et ce fait peut être exploité pour approfondir le partitionnement.
L'exemple classique est la FPU, qui regroupe un additionneur flottant et un multiplieur flottant. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions).
Pour l'ALU entière, les choses sont plus compliquées. L'ALU entière proprement dite effectue des ''opérations entières simples'', à savoir : additions, soustractions, comparaisons, opérations bit à bit et quelques autres opérations. Mais il y a aussi le multiplieur pour les multiplications, et le ''barrel shifter'' pour les décalages et rotations. Il est théoriquement possible de les partitionner, ce qui permettrait d'émettre en même temps : une opération entière simple, une multiplication, et un décalage/rotation. Faire cette séparation sera appelé le '''partitionnement entier''' dans ce qui suit.
En pratique, le gain en performance serait trop faible au regard des couts. Il est assez rare de devoir exécuter ces trois opérations en même temps. Aussi, le partitionnement entier n'est jamais utilisé tel quel. Il est par contre souvent mélangé avec la duplication des unités entières. Mais laissons cela pour plus tard.
[[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]]
===Le reconditionnement des unités fonctionnelles===
Le '''reconditionnement d'une unité fonctionnelle''' permet à celle-ci de faire des opérations qu'elle n'est pas prévue pour. Par exemple, il est possible d'utiliser l'unité mémoire comme seconde ALU entière, ou d'utiliser la FPU comme multiplieur. Ces deux exemples ne sont pas choisit au hasard, nous les étudierons dans ce qui suit. Le reconditionnement évite d'avoir à dupliquer une unité de calcul, tout en ajoutant une voie. Il permet d'améliorer l'efficacité du partitionnement, qu'il soit utilisé seul ou non.
Il est possible de reconditionner la FPU pour faire des multiplications entières. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits. Il est possible d'utiliser de multiplieur pour faire des multiplications entières 32 bits. Nous allons appeler cette possibilité le '''reconditionnement de la FPU'''.
Le reconditionnement de la FPU permet d'exécuter une multiplication entière en plus des autres opérations entières. Par exemple, prenons un CPU avec double émission entière-flottante, sans multiplieur entier séparé. Le reconditionnement de la FPU permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine.
Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants.
Passons maintenant à une seconde forme de reconditionnement. Reconditionner l'unité mémoire permet de l'utiliser comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, opérations bit à bit et quelques décalages limités (des décalages par 2, 4, 8, pas plus).
L'idée est d'utiliser l'AGU comme une seconde ALU entière. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. C'est une implémentation possible de l'''émission entière multiple'', vue plus haut. La technique est appelée le '''reconditionnement de l'AGU'''. L'implémentation demande d'ajouter une interconnexion entre la sortie de l'AGU et le banc de registre, précisément son port d'écriture. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]]
Idéalement, il faut que le jeu d'instruction supporte de quoi exploiter l'AGU pour faire des calculs entiers. Un exemple est celui des CPU x86, qui ont une instruction LEA (''Load Effective Adress''), qui effectue un calcul d'adresse dans l'AGU et mémorise le résultat dans un registre. Mais ce n'est pas nécessaire, le CPU peut reconnaitre les additions et décalages qui peuvent être émis dans l'AGU.
Un exemple est celui des CPU Intel 960 CA et CF, des versions améliorées de l'Intel 960. L'Intel 960 originel n'était pas superscalaire, mais la version 960 CA est reconnue comme le premier CPU RISC superscalaire au monde. Il intégrait une ALU entière, une unité mémoire et une unité de branchement. Grâce au partitionnement, il pouvait émettre une opération entière, un branchement et un accès mémoire en même temps. De plus, il utilisait l'AGU comme une seconde ALU entière, ce qui permettait d'émettre une seconde opération entière, en remplacement d'une µop mémoire. Son pipeline permettait donc les combinaisons INT + BRANCH + (MEM / + INT*).
Le cas de l'i960 nous montre que le reconditionnement permet d'améliorer l'intérêt du partitionnement. Elle permet de ne pas dupliquer d'unité de calcul, tout en permettant quand même d'émettre une seconde opération entière.
===La duplication des ALU entières===
Le partitionnement est une technique élégante, simple à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Même en utilisant le partitionnement intra-unité. La raison est que le partitionnement, permet d'exécuter des paires, triplets, quadruplets qui sont rares en pratique. Il est rare de devoir émettre en même temps un branchement, une opération entière, une opération flottante, et une opération mémoire.
Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''.
Le reconditionnement de l'unité mémoire permet de faire cela, en permettant d'émettre deux opérations entières simples. Heureusement, ce sont les plus fréquentes. Mais pour aller plus loin, les deux techniques précédentes ne suffisent plus. Il n'y a pas le choix : il faut dupliquer les ALU entières ! Il doit y avoir deux ALU entières pour de la double émission entière, trois pour de triple émission éntière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
[[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]]
La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications.
Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
|
* ALU entière
* Multiplieur
|
* ALU entière
* Barrel shifter
| FPU
|-
|
* Additions, soustractions, opérations bit à bit, etc.
* Multiplication
|
* Additions, soustractions, opérations bit à bit, etc.
* Décalages et rotations
| Opération flottante
|}
Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il faut noter que dupliquer l'ALU entière peut se marier parfaitement avec la présence d'une FPU. Les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU reconditionne sa FPU ou pas. Mais il est très rare qu'un CPU ait à la fois un multiplieur entier et une FPU reconditionnée.
{|class="wikitable"
|-
! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3
|-
| ALU entière || ALU entière || FPU
|}
Le processeur SuperSPARC avait deux ALU entière et une FPU. La FPU était reconditionnée, ce qui fait que la multiplication était faite dans la FPU, idem pour la division. Il lui est possible d'émettre deux opérations entières simples, en plus d'une multiplication. Par contre, on perd la possibilité d'émettre en même temps une multiplication et une opération flottante. Les combinaisons INT + INT + (MUL/FLOAT) sont possibles pas les combinaisons INT + MUL + FPU.
===La duplication des autres unités fonctionnelles===
Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale.
Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc.
La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement.
Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent.
Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications.
Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme.
===Les CPU superscalaires utilisent toutes ces stratégies===
Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, reconditionnement de l'unité mémoire, duplication de l'ALU entière, duplication des autre unités fonctionnelles. Le partitionnement a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. Le reconditionnement des unités a lui aussi un cout en circuit modéré, mais entraine des gains de performance bien plus significatifs, surtout pour le reconditionnement de l'AGU. La duplication des ALU entières est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. La duplication des autres unités a un cout en circuit variable, pour des gains en performance eux aussi variables. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor.
L'usage du partitionnement seul, sans duplication, était le fait de quelques design très anciens, pour les tout premiers CPU superscalaires. Et encore, beaucoup d'entre eux n'hésitaient pas à dupliquer leurs ALU entières. Le reconditionnement de l'AGU permet de bien mieux profiter du partitionnement seul, mais n'a pas beaucoup de sens quand on peut dupliquer des ALU entières. De nos jours, le partitionnement est utilisé en complément de la duplication des ALU entières.
Les CPU superscalaires les plus simples se contentent de dupliquer une ALU entière, sans partitionnement. Un exemple est le premier CPU superscalaire grand public : l'Intel Pentium. Il avait deux ALU entières, ce qui permettait d'émettre une opération entière INT en plus d'une seconde instruction. L'intel Pentium avait deux restrictions : impossible d’exécuter une opération entière en même temps qu'un branchement ou qu'une opération flottante. Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86.
Les processeurs superscalaires plus complexes utilisent partitionnement et duplication de l'ALU entière. Quelques CPU superscalaires anciens utilisaient ce combo, dans sa version la plus simple qui soit. C'était des CPU double émission, avec un partitionnement utilisant : deux ALUs entières, un multiplieur entier, une FPU et l'unité mémoire. L'alpha 21164 était dans ce cas, tout comme le PA-RISC 7200, le SuperSPARC, et le CPU des PowerPC G3 et G4. Ils pouvaient donc exécuter deux opérations entières en même temps, la seule contrainte étant qu'il ne pouvait y avoir une seule multiplication (il n'y a qu'un seul multiplieur).
Le SuperSPARC avait de nombreuses contraintes d’appariement. Par exemple, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Une bonne partie de ces contraintes était lié au banc de registre, qui n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions.
Les CPU superscalaires larges dupliquent les unités mémoire, de branchement, les multiplieurs et/ou les ''barrel shifters''. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'un branchement tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux branchements par paquet d'instruction, en moyenne. Avoir deux unités de branchement est alors un gain en performance clair.
==L'unité d'émission d'un processeur superscalaire==
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations.
Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Voyons pourquoi.
===L'excès de ports d'émission===
Prenons l'exemple d'un CPU superscalaire très large, avec 4 ALU entières, une FPU, et une unité mémoire. Il est particulièrement simple d'utiliser un port d'émission pour chaque unité de calcul. L'implémentation du processeur est en effet très simple si on fait ainsi. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. De plus, à quoi bon charger et décoder 6 instructions pour n'en utiliser en moyenne que 4 ou 5 ?
Une solution est de réduire la largeur du processeur. Dans l'exemple précédent, il est possible de passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatible avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres.
L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes.
Les contraintes d’appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes.
===Le partage des ports d'émission===
La solution précédente est certes très intéressante, mais elle a des défauts. Le principal est que les ports d'émission sont sous-utilisés. Et les ports d'émission ont un cout en circuits qui n'est pas négligeable. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul.
Un exemple très fréquent est lié aux circuits multiplieur et ''barrel shifters''. Si les anciens processeurs avaient un port d'émission dédié pour le multiplieur, voir le ''barrel shifter'', c'est très rare de nos jours. A la place, ils sont mis sur le même ports d'émission qu'une ALU entière. Un port d'émission est relié à une ALU entière et un ''barrel shifter'', un autre est relié à une ALU entière et un ''barrel shifter'', les autres ports d'émissions sont simplement reliés à une ALU entière.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]]
Pour reprendre l'exemple précédent, avec 6 unités de calcul. Il est possible de relier une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites.
Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement.
Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT).
: Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement.
Les deux techniques précédentes peuvent être utilisées en même temps, et le mieux est de l'illustrer par un exemple. Prenons les processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !
|-
| ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE
|-
| || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA)
|-
| || ''Barrel shifter'' || || || || Unité pour les instructions PUSH
|}
===L'intérieur de l'unité d'émission===
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''.
Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre.
Le ROB doit aussi être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle.
==Le ''front-end'' des processeurs superscalaires==
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont soit dupliquées, soit partitionnées, soit un mix des deux. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
* Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
* Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable.
Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance.
Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer,ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache.
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps.
Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops.
==Les µops mémoire sur les processeurs superscalaires==
Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une.
* Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières.
* Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''.
* Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''.
Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire.
===Les accès mémoire traités dans le pipeline entier===
Avec la double ou la triple émission, les accès mémoire sont traités comme des opérations entières un peu spéciales. En clair, le processeur peut émettre soit deux opérations entières, soit une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent.
Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ.
Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante...
===L'émission parallèle des accès mémoire===
A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle.
Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
===L'émission multiple des accès mémoire avec une LSQ===
Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''.
[[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]]
Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant.
===L'émission séparée des LOAD et STORE===
De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures.
En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture.
Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache.
[[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]]
L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée.
Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
==Les autres étages de pipeline d'un CPU superscalaire==
La superscalarité modifie en profondeur le processeur. Et cela dépasse le cadre des unités vues auparavant. Dans cette section, nous allons voir comment les autres étages du pipeline sont modifiées sur un CPU superscalaire.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===Le banc de registre d'un processeur superscalaire===
Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Une autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions auto-aligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions auto-aligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez longs, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les micro-architectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la micro-architecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
==La macro-fusion : une optimisation du décodage parallèle==
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP.
Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''.
Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques micro-architectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la micro-architecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la micro-architecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
mf8px7rgiwxncx0ykqii67l32uk7age
Cosmologie/L'univers observable
0
69553
772982
722560
2026-09-23T21:30:02Z
~2026-51373-26
124609
nous finiront -> nous finirons
772982
wikitext
text/x-wiki
L''''univers observable''' est la portion de l'univers que nous pouvons observer, compte tenu de la limite de la vitesse de la lumière. Des objets situés très loin ne peuvent pas être vus pour une raison très simple : la lumière qu'ils émettent n'a pas eu le temps de nous parvenir. La distance maximale à laquelle nous pouvons voir des objets (sans tenir compte d'éventuelles limitations techniques) dépend de l'âge de l'univers. S'il faut un temps supérieur à l'âge de l'univers pour nous parvenir, il nous est actuellement impossible de les voir, ce qui n'est pas le cas pour des objets situés plus près. Cette distance maximale est donc le rayon de l'univers observable. L'ensemble des points situés à la distance maximale des objets observables, à savoir la surface de l'univers observable, porte un nom : c'est l''''horizon cosmologique'''.
==Le rayon de Hubble==
On pourrait croire que le rayon de Hubble est une bonne estimation de la taille de l'univers observable. Pour rappel, ce rayon de Hubble est définit à partir du facteur de Hubble comme suit :
: <math>R_H(t) = c \cdot T_H</math>, avec c la vitesse de la lumière et <math>T_H</math> le temps de Hubble définit par <math>T_H = \frac{1}{H}</math>.
Il s'agit de la distance parcourue par la lumière pendant le temps de Hubble, lui-même définit comme l'inverse du facteur de Hubble. Mais le rayon de Hubble n'est malheureusement pas une bonne estimation du rayon de l'univers observable, pas plus que le temps de Hubble ne donne l'âge de l'univers. En effet, le rayon de Hubble n'est en rien la limite de l'univers observable : on voit des objets sont situés au-delà et on peut calculer leur ''redshift'' ou tout autre mesure pertinente.
Pourtant, sa définition semble indiquer que le rayon de Hubble soit la limite de l'univers observable. Par définition, tout ce qui est au-delà du rayon de Hubble va plus vite que la lumière. Donc la lumière émise au-delà de ce rayon n'est pas censée nous atteindre. Sauf que ce raisonnement par du principe que le rayon de Hubble est stable. Or, rappelez-vous l'équation du chapitre précédent, qui dit comment évolue le rayon de Hubble :
: <math>\frac{d}{dt} R_H(t) = c \cdot (q + 1)</math>
Pour notre univers, tout semble indiquer qu'il augmente avec le temps. Et cela explique pourquoi on peut voir des objets qui se déplacent "plus vite que la lumière", dans le sens où l'expansion les éloignait de nous plus vite que la lumière. La raison est que, vu que le rayon de Hubble grandit, certains photons hors du rayon de Hubble finissent par y rentrer. Ils sont en quelque sorte rattrapés par le rayon de Hubble et nous finirons donc par les voir une fois arrivés sur Terre. Par contre, les objets qui ont émis cette lumière s'éloignent bien de nous plus vite que la lumière et restent au-delà du rayon de Hubble.
==Le rayon de l'univers observable==
Le calcul du rayon cosmologique actuel est assez simple sur le principe, mais compliqué en pratique. Une méthode assez simple se base sur la ''vitesse d'éloignement de l'horizon'', à savoir la vitesse à laquelle l'horizon cosmologique s'éloigne de nous. Une fois cette vitesse connue, il suffit de l'intégrer sur l'âge de l'univers.
La vitesse d'éloignement du rayon observable est définie par :
: <math>v(t) = \frac{d}{dt} r(t)</math>, avec <math>r(t)</math> le rayon de l'univers à l'instant t.
On réutilise l'équation du chapitre précédent, qui donne la vitesse en fonction de la distance: <math>v(t) = v(t_0) a(t) + x(t_0) \cdot H(t)</math>. Dans le cas de l'horizon cosmologique, la distance est égale au rayon de l'univers et la vitesse locale est égale à la vitesse de la lumière, ce qui donne :
: <math> \frac{d}{dt} r(t) = c + H(t) \times r(t)</math>
On peut reformuler le tout en divisant par <math>r(t)</math>, ce qui donne :
: <math> \frac{r'(t)}{r(t)} = \frac{c}{r(t)} + H(t)</math>
On peut alors intégrer cette expression sur l'âge de l'univers pour obtenir le rayon de l'horizon cosmologique, du moins dans certaines conditions. Faire les calculs demande de connaître la loi d'évolution du facteur de Hubble <math>H(t) = ...</math>. Mais dans les faits, elle nous est inconnue et on n'en a que quelques bribes. La théorie ne nous est pas d'un grand secours et le seul cas que l'on peut calculer simplement est celui où le facteur de Hubble est constant. En général, les modèles cosmologiques les plus simples supposent que le facteur d'échelle suit une loi de puissance, de type <math>a(t) = a_0 \cdot (t/t_0)^{\beta}</math>, ce qui fait que l'équation peut se résoudre avec quelques développements analytiques. Mais des modèles plus réalistes ne suivent pas vraiment une loi de puissance, ce qui complique les calculs. Pour nous éviter de longs calculs fastidieux, nous allons étudier le cas général, en utilisant quelques raisonnements astucieux.
==Le rayon comobile de l'univers observable==
Une autre façon de faire les calculs est de passer par l'intermédiaire du rayon comobile. Pour rappel, ce rayon comobile est le rayon corrigé de l'influence du facteur d'échelle (et donc de l'expansion). Il vaut, par définition : <math>r(t) \over a(t)</math>.
La vitesse comobile est la dérivée de ce rayon comobile, qui est égale à :
: <math>\frac{d}{dt} \left( \frac{r(t)}{a(t)} \right) = \frac{r(t)'}{a(t)} - \frac{r(t) \cdot a(t)'}{a(t)^2} = \frac{r(t)'}{a(t)} - H(t) \frac{r(t)}{a(t)}</math>
On peut alors factoriser le rayon comobile <math>r(t) \over a(t)</math> :
: <math>\frac{d}{dt} \left( \frac{r(t)}{a(t)} \right) = \frac{r(t)}{a(t)} \left[ \frac{r(t)'}{a(t)} \frac{a(t)}{r(t)} - H(t) \right]</math>
On simplifie :
: <math>\frac{d}{dt} \left( \frac{r(t)}{a(t)} \right) = \frac{r(t)}{a(t)} \left[ \frac{r(t)'}{r(t)} - H(t) \right]</math>
En injectant l'équation <math> \frac{r(t)'}{r(t)} = \frac{c}{r(t)} + H(t) </math> dans la précédente, on a :
: <math>\frac{d}{dt} \left( \frac{r(t)}{a(t)} \right) = \frac{r(t)}{a(t)} \left[ \frac{c}{r(t)} + H(t)-H(t) \right]</math>
En développant, on trouve :
: <math>\frac{d}{dt} \left( \frac{r(t)}{a(t)} \right) = \frac{c}{a(t)}</math>
On voit que la dérivée est égale à ce qu'on appelle la vitesse comobile de la lumière. Par définition, la vitesse de la lumière est de <math>c = 299 792 , 458 Km/S</math>, mais il s'agit d'une vitesse propre. On peut calculer sa vitesse comobile en divisant par le facteur d'échelle, ce qui n'est autre que le premier terme de l'équation précédente. <math> c_{com} = {c \over a} </math>. En intégrant l'équation précédente sur l'âge de l'univers, on a la distance comobile de l'horizon, celle à laquelle se situait l'horizon cosmologique quand la lumière de l'horizon a été émise. On se retrouve alors avec une équation très générale, qui marche même quand le facteur de Hubble est variable.
Seule la vitesse comobile de la lumière devant être prise en compte. On trouve alors que le rayon comobile se calcule avec la formule suivante, avec <math>t_u</math> l'âge de l'univers :
: <math> \frac{r(t)}{a(t)} = \int_{0}^{t_u} {c \over a(t)} dt = c \int_{0}^{t_u} {dt \over a(t)}</math>
Il est possible de simplifier fortement la formule précédente en faisant intervenir une variable particulière, appelée le '''temps conforme''', définie par <math>\eta = \frac{dt}{a(t)}</math>. On peut voir ce temps conforme comme l'équivalent de la distance comobile, mais pour les durées. Il n'a pas vraiment d'interprétation physique digne de ce nom et sert plus d'intermédiaire pour les calculs. Avec le temps conforme, la formule précédente devient alors :
: <math> \frac{r(t)}{a(t)} = c \cdot (\eta_0 - \eta_{t_u})</math>
: ''On peut voir la formule précédente comme la généralisation de la formule <math>D = c \cdot \Delta T</math>, mais où le temps est remplacé par le temps conforme.''
==Le rayon actuel de l'univers observable==
On peut obtenir la distance propre par un calcul très simple, à partir de la distance comobile. Le passage de la distance comobile à la distance propre se fait simplement en multipliant par <math>a(t)</math>. On obtient alors la distance propre suivante :
: <math>r(t) = a(t) \cdot \int_{0}^{t_u} {c \over a(t)} dt</math>
Dans l'équation précédente, on peut factoriser la vitesse de la lumière, ce qui donne :
: <math>r(t) = c \cdot \left[ a(t) \cdot \int_{0}^{t} {1 \over a(t_i)} dt_i \right]</math>
: ''Comme pour le rayon comobile, on peut voir la formule précédente comme la généralisation de la formule <math>D = c \cdot \Delta T</math>, mais où la durée <math>\Delta T</math> est remplacée par la valeur temporelle <math>\left[ a(t) \cdot \int_{0}^{t} {1 \over a(t_i)} dt_i \right]</math>.''
Les intégrales précédentes ne sont pas solubles telles quelles. Il faut préciser comment le facteur d'échelle évolue avec le temps, sans quoi les intégrales ne peuvent pas être calculées exactement. Manque de chance, la loi d'évolution du facteur d'échelle nous est inconnue. Nous sommes obligés de postuler des fonctions particulières, en espérant qu'elles collent au mieux aux observations. Nous ferons cela plus en détail dans le chapitre sur les modèles cosmologiques.
{{NavChapitre | book=Cosmologie
| prev=Le facteur de Hubble
| prevText=Le facteur de Hubble
| next=Une introduction aux modèles cosmologiques
| nextText=Une introduction aux modèles cosmologiques
}}
[[Catégorie:Cosmologie (livre)|*]]
2l7e576fywn5qltscpbpmo7o9z1svqd
Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions
0
79799
772969
772808
2026-09-23T21:05:34Z
Mewtow
31375
/* Le Branch Folding des CPU PowerPC */
772969
wikitext
text/x-wiki
Les processeurs avec un pipeline sont découpés en deux sections : un ''front-end'' qui charge et décode les instructions, et un ou plusieurs ''back-end'' qui exécutent les instructions. L'exécution des micro-opérations est optimisée par des techniques que nous verrons dans la suite du cours : exécution dans le désordre, superscalarité, renommage de registre. Mais un point important est que le ''front-end'' a des optimisations dédiées. La prédiction de branchement est l'une de ces optimisation, mais elle n'est pas la seule. Ce chapitre aborde les optimisations autres que la prédiction de branchement. Nous allons voir le découplage des étages du ''front-end'', le pré-décodage et quelques améliorations liées indirectement à la prédiction de branchement.
==Le découplage du ''front-end''==
Le séquenceur d'un processeur contient une unité de chargement, un décodeur et un chemin de données. Avec un pipeline, le couplage de ces structures fait que si l'une d'entre elle prend plus d'un cycle pour faire son travail, les étages précédents et/ou suivants sont stoppés. Par exemple, lors d'un défaut dans le cache d'instruction, l'ensemble stoppe. De même, si jamais le décodeur doit décoder une instruction via le micro-code, cela prend plusieurs cycles : l'unité de chargement et le cache d'instruction sont inutilisés. Même chose si jamais une instruction multicycle s’exécute dans le pipeline : cela bloque toutes les étapes précédentes.
Mais il s'agit là d'un défaut inhérent aux pipelines basiques, qui relient chaque étage avec des registres. Avec un peu d'astuce, il est possible que certains étages prennent de l'avance même si l'étage suivant est bloqué. Par exemple, si le décodeur bloque le pipeline en utilisant son micro-code pendant 4-5 cycles, l'unité de chargement peut en théorie précharger à l'avance les instructions suivantes, et les mettre en attente. Ou encore, en cas de bulle de pipeline (''pipeline stall''), le décodeur peut décoder en avance des instructions et les mettre en attente tant que la bulle de pipeline est en cours.
Dans les deux cas, une unité prend de l'avance et met en attente ses résultats tant que l'étage suivant est occupé. La mise en attente est réalisée en remplaçant/complémentant les registres du pipeline avec des mémoires FIFOs. Elles remplacent ou complémentant les registres de pipeline, les deux sont possibles. Si elles remplacent ces registres, si aucune mise en attente n'est requise, elles fonctionnent comme un registre de pipeline.
===La file d'instruction===
[[File:File de micro-opérations.png|vignette|upright=1|File d'instructions]]
Les processeurs modernes intègrent une '''file d'instruction''', à savoir une mémoire FIFO placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la file d'instructions et sont décodées quand l'unité de décodage est prête. La file d'attente permet de charger des instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs.
L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur exécute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours. La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===La file de micro-opérations===
[[File:File de micro-opération.png|vignette|upright=1|File d'instruction]]
L'optimisation précédente peut aussi s'appliquer entre le décodeur d'instruction et le chemin de données. Pour cela, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l'appellerons la '''file de micro-opérations''', mais elle porte de nombreux noms et la terminologie des différents fabricants est assez confuse. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut charger et décoder des instructions, il a juste à accumuler les instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions décodées quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Pendant un défaut de cache, aucune instruction n'est chargée ni décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il se rattrape en émettant les instructions en attente dans la file de micro-opérations, s'il y en a. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais nous verrons cela dans plusieurs chapitres). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple utiliser la file de micro-opérations comme une sorte de pseudo-cache FIFO. La file de micro-opérations ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Un circuit annexe, appelé le '''''Loop Stream Detector''''' (LSD), détecte les boucles dans la file de micro-opérations et optimise leur exécution. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, qui tiennent toutes entières dans la file de micro-opérations, et sous condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
Le LSD vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les micro-architectures Zen 4 mais il a disparu sur la micro-architecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la micro-architecture Zen 4 d'AMD ou les CPU de micro-architecture Skylake et Kaby Lake d'Intel. Pour la micro-architecture Skylake, les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle accumule les instructions décodées dans le cache de micro-opérations, les itérations suivantes de la boucle lisent les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois. Sur de nombreux processeurs, le cache de micro-opération est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ainsi, seules les micro-opérations émises sont copiées dans ce cache.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
Une différence avec le cache de micro-opération est qu'une boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, un cache de macro/micro-opération mémorisera les micro-opérations du IF et du ELSE et les fournira selon les besoins. La raison est que le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', pour un cout en transistor plus élevé.
Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de micro-architecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. Une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de son envoi au chemin de données, lors de son émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Nous venons de voir qu'il est possible de découpler les étages de chargement, décodage et le chemin de données, en insérant des mémoires FIFOs dans le pipeline. Il en est de même avec l'unité de chargement elle-même. Elle est composée de deux circuits entre lesquels on peut ajouter des mémoires FIFO : une unité de calcul d'adresse et le cache d'instruction. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction.
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de calcul d'adresse de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Le prédécodage d'instructions==
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple.
Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction.
===Les sélecteurs de branchement intégrés au cache L1===
Le pré-décodage peut être utilisé afin de faciliter le travail de la prédiction de branchement. L'idée est d'incorporer une partie de la prédiction de branchement dans le cache L1 d'instruction. Une ligne de cache mémorise alors des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non. Il s'agit d'une démarche inverse à celle de la ''Fetch Target Queue'', qui découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Là, la démarche est au contraire de fusionner partiellement cache d'instruction et unité de prédiction de branchement.
Les premiers processeurs AMD utilisaient cette technique, au moins dans les grandes lignes. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction===
Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment.
L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela.
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''. Le mécanisme pour adresser ce cache à partir du cache d'instruction n'est pas très détaillé dans la documentation d'AMD.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
==Le ''Branch Folding'' : l'exécution anticipée des branchements==
Le '''''Branch Folding''''' est une optimisation qui permet d'exécuter les branchements en avance, pendant qu'ils sont encore dans la file d'instruction. Pour cela, une unité spécialisée scanne la file d'instruction pour y trouver les branchements. Quand elle tombe sur un branchement, elle l'exécute en avance et altère le contenu de la file d'instruction pour remplacer les instructions chargées à tord. L'unité en question s'appelle l''''unité de branchements anticipés'''.
Pour les branchements inconditionnels, elle peut les "exécuter directement". Elle retire le branchement et le remplace par l'instruction de destination du branchement. Le ''program counter'' est aussi altéré en avance, ce qui fait que les instructions adéquates sont ensuite chargées dans la file d'instruction. Pour les branchements conditionnels, tout dépend de si le processeur implémente la prédiction de branchement ou non.
Sans prédiction de branchement, le mieux est de traiter les branchements conditionnels comme non-pris et prier pour que ça fonctionne. Avec prédiction de branchement, tout dépend de si le branchement est prédit comme pris ou non. Si le branchement est considéré comme pris, il est traité comme un branchement inconditionnel : il est remplacé par l'instruction de destination et le ''program counter'' est altéré. SI le branchement est non-pris, il suffit de ne rien faire. Notez qu'ils faut que l'adresse de destination soit connue à l'avance, ou du moins puisse être calculée facilement (branchements relatifs).
Les premiers processeurs PowerPC utilisaient cette optimisation. Ils avaient une file d'instruction de 8 instructions, et l'unité de branchement scannait les 4 premières instructions. L'unité de branchements anticipés contenait les registres nécessaires pour gérer les branchements. Elle contenait précisément le ''link register'' pour des appels de procédures, le ''Count Target Register'' et le ''Count Register'' utilisés pour les boucles. Il contient aussi un additionneur pour calculer les adresses des branchements relatifs.
L'unité de branchements anticipés doit identifier les branchements, ce qui demande de décoder partiellement les instructions en attente. Une solution pratique est d'utiliser la technique du pré-décodage d'instruction, de la section précédente. Le pré-décodage est parfait pour identifier les branchements dans un bloc d'instruction. Les informations prédécodées peuvent alors être utilisées pour remplir la file d'instruction. Chaque instruction dans la file d'instruction est associée à un bit qui indique si c'est un branchement ou non. L'unité de branchement anticipé peut alors détecter les branchements avec un simple encodeur à priorité.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=Les pipelines multicycles
| nextText=Les pipelines multicycles
}}
</noinclude>
{{AutoCat}}
hd4xlan38qy5y9sxdvoadljt6utddfm
Mathc initiation/a522
0
80975
772960
772812
2026-09-23T20:44:42Z
Xhungab
23827
772960
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Sommaire#Analyse IV|Sommaire]]
Je vous propose comme cours de référence les trois livres de '''openstax''' en accès libre. Vous pouvez les lire en ligne ou télécharger les PDF.
* [https://openstax.org/details/books/calculus-volume-1 Calculus 1],
* [https://openstax.org/details/books/calculus-volume-2 Calculus 2],
* [https://openstax.org/details/books/calculus-volume-3 Calculus 3].
:
.
:
{{Partie{{{type|}}}|[[Mathc initiation/a10| Analyse IV : Les équations différentielles]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a594| Analyse IV : Se familiariser avec les séries de Fourier]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a592| Analyse IV : Se familiariser avec la transformée de Fourier discrète]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a512| Analyse IV : Se familiariser avec la Transformée de Laplace]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006y| Analyse IV : la Transformée Inverse de Laplace]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006s| Analyse IV : la Transformée de Laplace : Quelques exercices]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006r| Analyse IV : la Transformée de Laplace : Quelques applications]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a584| Analyse IV : Se familiariser avec la transformée en Z]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0075| Analyse IV : la Transformée en Z : Quelques applications]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0073| Analyse IV : La transformée en Z : Quelques astuces]]}}
{{AutoCat}}
j1z180iaw65bugy6zaznc8uv9esrjl4
772999
772960
2026-09-24T07:01:09Z
Xhungab
23827
772999
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Sommaire#Analyse IV|Sommaire]]
Je vous propose comme cours de référence les trois livres de '''openstax''' en accès libre. Vous pouvez les lire en ligne ou télécharger les PDF.
* [https://openstax.org/details/books/calculus-volume-1 Calculus 1],
* [https://openstax.org/details/books/calculus-volume-2 Calculus 2],
* [https://openstax.org/details/books/calculus-volume-3 Calculus 3].
:
.
:
{{Partie{{{type|}}}|[[Mathc initiation/a10| Analyse IV : Les équations différentielles]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a594| Analyse IV : Se familiariser avec les séries de Fourier]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a592| Analyse IV : Se familiariser avec la transformée de Fourier discrète]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a512| Analyse IV : Se familiariser avec la Transformée de Laplace]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006y| Analyse IV : la Transformée Inverse de Laplace]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006s| Analyse IV : la Transformée de Laplace : Quelques exercices]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006r| Analyse IV : la Transformée de Laplace : Quelques applications]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a584| Analyse IV : Se familiariser avec la transformée en Z]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a583| Analyse IV : Quelques propriétés des transformées en Z]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0075| Analyse IV : la Transformée en Z : Quelques applications]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0073| Analyse IV : La transformée en Z : Quelques astuces]]}}
{{AutoCat}}
e7db8161h5vbl7gijuh1q0ilz7928b5
773001
772999
2026-09-24T07:04:20Z
Xhungab
23827
773001
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Sommaire#Analyse IV|Sommaire]]
Je vous propose comme cours de référence les trois livres de '''openstax''' en accès libre. Vous pouvez les lire en ligne ou télécharger les PDF.
* [https://openstax.org/details/books/calculus-volume-1 Calculus 1],
* [https://openstax.org/details/books/calculus-volume-2 Calculus 2],
* [https://openstax.org/details/books/calculus-volume-3 Calculus 3].
:
.
:
{{Partie{{{type|}}}|[[Mathc initiation/a10| Analyse IV : Les équations différentielles]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a594| Analyse IV : Se familiariser avec les séries de Fourier]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a592| Analyse IV : Se familiariser avec la transformée de Fourier discrète]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a512| Analyse IV : Se familiariser avec la Transformée de Laplace]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006y| Analyse IV : la Transformée Inverse de Laplace]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006s| Analyse IV : la Transformée de Laplace : Quelques exercices]]}}
{{Partie{{{type|}}}|[[Mathc initiation/006r| Analyse IV : la Transformée de Laplace : Quelques applications]]}}
.
{{Partie{{{type|}}}|[[Mathc initiation/a584| Analyse IV : Se familiariser avec la transformée en Z]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a583| Analyse IV : la Transformée en Z : Quelques propriétés]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0075| Analyse IV : la Transformée en Z : Quelques applications]]}}
{{Partie{{{type|}}}|[[Mathc initiation/0073| Analyse IV : La transformée en Z : Quelques astuces]]}}
{{AutoCat}}
nh84gyjimxu5aue16tohkwiq6qy2bfx
Mathc initiation/a584
0
81062
772989
772142
2026-09-23T21:46:49Z
Xhungab
23827
772989
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la Transformée de Laplace|Sommaire]]
{{Partie{{{type|}}}|Se familiariser avec la transformée en Z :}}
Soit x[n] un signal causal discret :
_oo+__
\
La transformée en Z de x[n] est X(z) = | x[n] z^(-n)
/_____
n = 0
1 1 1 1
Soit = x(0) ----- + x(1) ----- + x(2) ----- + ... + xn) -----
z^(0) z^(1) z^(2) z^(n)
{{Partie{{{type|}}}|[[Mathc initiation/a581|* Les suites usuelles :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a582|* Transformées en Z usuelles, des signaux causales discrets ::]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a583|* Propriétés des transformées en Z : ]]}}
Se familiariser avec '''la transformée en Z Inverse''' :
Pour ce familiariser avec '''la transformée en Z''', je vous conseille de voir les sections précédentes en étudiant les colonnes de gauche à droite.
Pour '''la transformée en Z inverse''', il suffit de travailler avec les colonnes de droite à gauche.
Il faudra se familiariser avec [[Mathc initiation/a569|la méthode des fractions partielles]], et la division des polynômes. Vous pouvez revoir [[Mathc initiation/Fichiers h : c18|La méthode de Horner (Factoriser, diviser des polynômes)]]
Les méthodes plus avancées dépassent mes compétences.
:
{{AutoCat}}
if2whsj0q96465i8b3n9b5wzw0ktzvy
772990
772989
2026-09-23T21:48:12Z
Xhungab
23827
772990
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la Transformée de Laplace|Sommaire]]
{{Partie{{{type|}}}|Se familiariser avec la transformée en Z :}}
Soit x[n] un signal causal discret :
_oo+__
\
La transformée en Z de x[n] est X(z) = | x[n] z^(-n)
/_____
n = 0
1 1 1 1
Soit = x(0) ----- + x(1) ----- + x(2) ----- + ... + xn) -----
z^(0) z^(1) z^(2) z^(n)
{{Partie{{{type|}}}|[[Mathc initiation/a581|* Les suites usuelles :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a582|* Transformées en Z usuelles, des signaux causales discrets ::]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a583|* Propriétés des transformées en Z : ]]}}
Se familiariser avec '''la transformée en Z Inverse''' :
Pour ce familiariser avec '''la transformée en Z''', je vous conseille de voir les sections précédentes en étudiant les colonnes de gauche à droite.
Pour '''la transformée en Z inverse''', il suffit de travailler avec les colonnes de droite à gauche.
Il faudra se familiariser avec [[Mathc initiation/a569|la méthode des fractions partielles]], et la division des polynômes. Vous pouvez revoir [[Mathc initiation/Fichiers h : c18|La méthode de Horner (Factoriser, diviser des polynômes)]]
Les méthodes plus avancées dépassent mes compétences.
:
{{AutoCat}}
78l41k4wlxnn6ynopm79xxui49nu5cg
772991
772990
2026-09-23T21:49:22Z
Xhungab
23827
772991
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|Se familiariser avec la transformée en Z :}}
Soit x[n] un signal causal discret :
_oo+__
\
La transformée en Z de x[n] est X(z) = | x[n] z^(-n)
/_____
n = 0
1 1 1 1
Soit = x(0) ----- + x(1) ----- + x(2) ----- + ... + xn) -----
z^(0) z^(1) z^(2) z^(n)
{{Partie{{{type|}}}|[[Mathc initiation/a581|* Les suites usuelles :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a582|* Transformées en Z usuelles, des signaux causales discrets ::]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a583|* Propriétés des transformées en Z : ]]}}
Se familiariser avec '''la transformée en Z Inverse''' :
Pour ce familiariser avec '''la transformée en Z''', je vous conseille de voir les sections précédentes en étudiant les colonnes de gauche à droite.
Pour '''la transformée en Z inverse''', il suffit de travailler avec les colonnes de droite à gauche.
Il faudra se familiariser avec [[Mathc initiation/a569|la méthode des fractions partielles]], et la division des polynômes. Vous pouvez revoir [[Mathc initiation/Fichiers h : c18|La méthode de Horner (Factoriser, diviser des polynômes)]]
Les méthodes plus avancées dépassent mes compétences.
:
{{AutoCat}}
atjf1a8kqr11ex650qshzj22xlac7tj
772998
772991
2026-09-24T06:58:39Z
Xhungab
23827
772998
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|Se familiariser avec la transformée en Z :}}
Soit x[n] un signal causal discret :
_oo+__
\
La transformée en Z de x[n] est X(z) = | x[n] z^(-n)
/_____
n = 0
1 1 1 1
Soit = x(0) ----- + x(1) ----- + x(2) ----- + ... + xn) -----
z^(0) z^(1) z^(2) z^(n)
{{Partie{{{type|}}}|[[Mathc initiation/a581|* Les suites usuelles :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a582|* Transformées en Z usuelles, des signaux causales discrets ::]]}}
Se familiariser avec '''la transformée en Z Inverse''' :
Pour ce familiariser avec '''la transformée en Z''', je vous conseille de voir les sections précédentes en étudiant les colonnes de gauche à droite.
Pour '''la transformée en Z inverse''', il suffit de travailler avec les colonnes de droite à gauche.
Il faudra se familiariser avec [[Mathc initiation/a569|la méthode des fractions partielles]], et la division des polynômes. Vous pouvez revoir [[Mathc initiation/Fichiers h : c18|La méthode de Horner (Factoriser, diviser des polynômes)]]
Les méthodes plus avancées dépassent mes compétences.
:
{{AutoCat}}
qtbjydrnti7v7v0o0rlg09s5akjqluu
Mathc initiation/a581
0
81063
772993
772139
2026-09-23T21:53:37Z
Xhungab
23827
772993
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
:
{{Partie{{{type|}}}|Les suites usuelles :}}
* '''L'impulsion unitaire :'''
δ[n] : Si n = 0 alors '''δ[0] = 1''' si n != 0 alors δ(n) = 0
* '''L'échelon unitaire :'''
u[n] : Si n >= 0 alors '''u[n] = 1''' si n < 0 alors u(n) = 0
* '''La rampe :'''
r[n] : Si n >= 0 alors '''r[n] = n''' si n < 0 alors r(n) = 0
* '''Le carré :'''
c[n] : Si n >= 0 alors '''c[n] = n^2''' si n < 0 alors c(n) = 0
* '''L'exponentiel :'''
f[n] : Si n >= 0 alors '''f[n] = a^n''' si n < 0 alors c(n) = 0
* '''La fonction cosinus discret :'''
g[n] : Si n >= 0 alors '''g[n] = cos(kn)''' si n < 0 alors c(n) = 0
* '''La fonction sinus discret :'''
h[n] : Si n >= 0 alors '''h[n] = sin(kn)''' si n < 0 alors c(n) = 0
:
----
{{AutoCat}}
kmjt8hms0x7jmkjrck71kite7hb4m5l
Mathc initiation/a582
0
81064
772994
772698
2026-09-23T21:55:23Z
Xhungab
23827
772994
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence
'''x[n]''' '''X(z)'''
'''δ[n] = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u[n] = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire'''
'''r[n] = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe'''
'''c[n] = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré'''
'''f[n] = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel'''
'''g[n] = cos(kn)''' '''[[Mathc initiation/0071#La fonction cosinus discret : g(n) = cos(kn)|[z^2-z cos(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction cosinus discret'''
'''h[n] = sin(kn)''' '''[[Mathc initiation/0072#La fonction sinus discret : h(n) = sin(kn)|[z sin(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction sinus discret'''
{{AutoCat}}
t6ejf7g5l5lia560fqp9mejao1getkw
772995
772994
2026-09-23T21:55:40Z
Xhungab
23827
772995
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 :}}
'''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence
'''x[n]''' '''X(z)'''
'''δ[n] = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u[n] = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire'''
'''r[n] = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe'''
'''c[n] = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré'''
'''f[n] = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel'''
'''g[n] = cos(kn)''' '''[[Mathc initiation/0071#La fonction cosinus discret : g(n) = cos(kn)|[z^2-z cos(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction cosinus discret'''
'''h[n] = sin(kn)''' '''[[Mathc initiation/0072#La fonction sinus discret : h(n) = sin(kn)|[z sin(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction sinus discret'''
{{AutoCat}}
6auoj2dzip96pien96vugrrcydwpikq
772996
772995
2026-09-23T21:56:22Z
Xhungab
23827
772996
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 :}}
'''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence
'''x[n]''' '''X(z)'''
'''δ[n] = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u[n] = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire'''
'''r[n] = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe'''
'''c[n] = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré'''
'''f[n] = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel'''
'''g[n] = cos(kn)''' '''[[Mathc initiation/0071#La fonction cosinus discret : g(n) = cos(kn)|[z^2-z cos(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction cosinus discret'''
'''h[n] = sin(kn)''' '''[[Mathc initiation/0072#La fonction sinus discret : h(n) = sin(kn)|[z sin(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction sinus discret'''
{{AutoCat}}
n4ngn1wsfpkt8fjihyuse1go9ebss6e
772997
772996
2026-09-23T21:57:41Z
Xhungab
23827
772997
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 :}}
'''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence
'''x[n]''' '''X(z)'''
'''δ[n] = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u[n] = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire'''
'''r[n] = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe'''
'''c[n] = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré'''
'''f[n] = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel'''
'''g[n] = cos(kn)''' '''[[Mathc initiation/0071#La fonction cosinus discret : g(n) = cos(kn)|[z^2-z cos(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction cosinus discret'''
'''h[n] = sin(kn)''' '''[[Mathc initiation/0072#La fonction sinus discret : h(n) = sin(kn)|[z sin(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction sinus discret'''
{{AutoCat}}
d3nz0qc163zynjcadi47mymd377kc7c
Mathc initiation/a583
0
81065
773000
772811
2026-09-24T07:03:57Z
Xhungab
23827
773000
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|la Transformée en Z : Quelques propriétés}}
Soit x(n) et y(n) deux signaux causaux discrets :
'''* La transformée en Z est un opérateur linéaire :'''
'''Z[a x(n) + b y(n)] = a Z[x(n)] + b Z[y(n)] = a X(z) + b Y(z)'''
'''* Multiplication par une exponentielle : [[Mathc initiation/a585|Exemples]] '''
a^n x(n)
Si Z[x(n)] = X(z) alors '''Z[a^n x(n)] = X(z/a)'''
'''* Multiplication par la variable d'évolution : [[Mathc initiation/a588|Exemples]] '''
n x(n)
Si Z[x(n)] = X(z) alors '''Z[n x(n)] = -z X'(z)'''
'''* Le retard de no unités : [[Mathc initiation/a589|Exemples]] '''
x(n-no) est un signal causal retardé de no unités.
Image (L'invité retardé de no minutes, arrivera à 12h+no minutes)
Si Z[x(n)] = X(z) alors '''Z[x(n-no)] = z^(-no) X(z)'''
'''* L'avance d'une unité : [[Mathc initiation/a590|Exemples]] '''
x(n+1) est un signal causal avancé de 1 unité.
Image (L'invité en avance de 1 minute, arrivera à 12h-1 minute, 11h59 minutes)
Si Z[x(n)] = X(z) alors '''Z[x(n+1)] = z^(1) [X(z)-x(0)]'''
'''* L'avance de deux unités : [[Mathc initiation/a591|Exemples]]'''
x(n+2) est un signal causal avancé de 2 unité.
Image (L'invité en avance de 2 minutes, arrivera à 12h-2 minutes, 11h58 minutes)
Si Z[x(n)] = X(z) alors '''Z[x(n+2)] = z^(2) [X(z)-x(0)-x(1)z^(-1)]'''
Remarque :
Lors d'une avance, certains éléments de la suite x(n) peuvent passer du côté négatif.
Dans ce cas, il suffit d'ignorer ces éléments :
-1 0 1 2 3 4
x(0) x(1) x(2) x(3) x(4)
<- x(0) x(1) x(2) x(3) x(4) x(5) L'avance d'une unité.
x(1) x(2) x(3) x(4) x(5)
{{AutoCat}}
rr7ijzjna6zb8kczqqjkyq559jintas
773005
773000
2026-09-24T07:21:47Z
Xhungab
23827
773005
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|la Transformée en Z : Quelques propriétés}}
Soit x[n] et y[n] deux signaux causaux discrets :
'''* La transformée en Z est un opérateur linéaire :'''
'''Z(a x[n] + b y[n]) = a Z(x[n]) + b Z(y[n]) = a X(z) + b Y(z)'''
{{Partie{{{type|}}}|[[Mathc initiation/a585|* Multiplication par une exponentielle :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a588|* Multiplication par la variable d'évolution :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a589|* Le retard de no unités :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a590|* L'avance d'une unité :]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a591|* L'avance de deux unités :]]}}
{{AutoCat}}
ossic4uvbkf9ieqnvthgku10zxu5y0u
773006
773005
2026-09-24T07:29:03Z
Xhungab
23827
773006
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|la Transformée en Z : Quelques propriétés}}
Soit x[n] et y[n] deux signaux causaux discrets :
'''* La transformée en Z est un opérateur linéaire :'''
'''Z(a x[n] + b y[n]) = a Z(x[n]) + b Z(y[n]) = a X(z) + b Y(z)'''
{{Partie{{{type|}}}|[[Mathc initiation/a585|* Multiplication par une exponentielle : '''Z(a^n x(n)) = X(z/a)''']]}}
{{Partie{{{type|}}}|[[Mathc initiation/a588|* Multiplication par la variable d'évolution : '''Z(n x(n)) = -z X'(z)''']]}}
{{Partie{{{type|}}}|[[Mathc initiation/a589|* Le retard de no unités : '''Z(x(n-no)) = z^(-no) X(z)''']]}}
{{Partie{{{type|}}}|[[Mathc initiation/a590|* L'avance d'une unité : '''Z[x(n+1)] = z^(1) (X(z)-x(0))''']]}}
{{Partie{{{type|}}}|[[Mathc initiation/a591|* L'avance de deux unités : '''Z[x(n+2)] = z^(2) (X(z)-x(0)-x(1)z^(-1))''']]}}
{{AutoCat}}
peqzlfausvqoe8po9donj3g3aeeb8tj
Mathc initiation/a585
0
81067
772977
772706
2026-09-23T21:20:18Z
Xhungab
23827
772977
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n x(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Si Z[x(n)] = X(z) alors '''Z[a^n x(n)] = X(z/a)'''
'''Le signal''' '''Transformée en Z'''
'''x(n)''' '''X(z)''' '''Z[a^n x(n)] = X(z/a)'''
'''u(n)''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal g(n) : '''cos(kn)'''
Transformée en Z G(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n g(n)] : '''[(z/a)^2-(z/a)cos(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
G(z/a) : '''[z^2-az cos(k)]/[z^2-2az cos(k)+a^2]'''
Le signal h(n) : '''sin(kn)'''
Transformée en Z H(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n h(n)] : '''[(z/a) sin(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
H(z/a) : '''[az sin(k)]/[z^2-2az cos(k)+a^2]'''
{{AutoCat}}
s6fzf7lxq60tlsixzx33p8adhlxgcgq
772978
772977
2026-09-23T21:21:53Z
Xhungab
23827
772978
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n x(n) u(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Si Z[x[n]] = X(z) alors '''Z[a^n x[n]] = X(z/a)'''
'''Le signal''' '''Transformée en Z'''
'''x[n]''' '''X(z)''' '''Z[a^n x[n]] = X(z/a)'''
'''u[n]''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal g[n] : '''cos(kn)'''
Transformée en Z G(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n g[n]] : '''[(z/a)^2-(z/a)cos(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
G(z/a) : '''[z^2-az cos(k)]/[z^2-2az cos(k)+a^2]'''
Le signal h[n] : '''sin(kn)'''
Transformée en Z H(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n h[n]] : '''[(z/a) sin(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
H(z/a) : '''[az sin(k)]/[z^2-2az cos(k)+a^2]'''
{{AutoCat}}
owmwa65pcujcy0d74yqy91376h1z217
772980
772978
2026-09-23T21:24:41Z
Xhungab
23827
772980
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|Multiplication par une exponentielle : a^n x(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Si Z[x[n]] = X(z) alors '''Z[a^n x[n]] = X(z/a)'''
'''Le signal''' '''Transformée en Z'''
'''x[n]''' '''X(z)''' '''Z[a^n x[n]] = X(z/a)'''
'''u[n]''' '''z/(z-1)''' '''(z/a)/((z/a)-1)''' = '''z/(z-a)'''
'''n''' '''z/(z-1)^2''' '''(z/a)/((z/a)-1)^2''' = '''az/(z-a)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''(z/a)((z/a)+1)/((z/a)-1)^3''' = '''(a+z)az/(z-a)^3'''
'''a^n''' '''z/(z-a)''' '''(z/a)/((z/a)-a)''' = '''z/(z-a^2)'''
Le signal g[n] : '''cos(kn)'''
Transformée en Z G(z) : '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
Z[a^n g[n]] : '''[(z/a)^2-(z/a)cos(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
G(z/a) : '''[z^2-az cos(k)]/[z^2-2az cos(k)+a^2]'''
Le signal h[n] : '''sin(kn)'''
Transformée en Z H(z) : '''[z sin(k)]/[z^2-2z cos(k)+1]'''
Z[a^n h[n]] : '''[(z/a) sin(k)]/[(z/a)^2-2(z/a)cos(k)+1]'''
H(z/a) : '''[az sin(k)]/[z^2-2az cos(k)+a^2]'''
{{AutoCat}}
kq24jgvnfsv3rl8ilx45p38f3dz03v5
Mathc initiation/a588
0
81071
772983
772656
2026-09-23T21:30:15Z
Xhungab
23827
772983
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n x[n]|fond={{{fond|}}<nowiki>}</nowiki>}}
'''Le signal''' '''Transformée en Z'''
'''x[n]''' '''X(z)''' '''Z[n x[n]] = -z X'(z)'''
'''u[n] = 1''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''r[n] = n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''c[n] = n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''f[n] = a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal g[n]''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n g[n]]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z G'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal h[n]''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n h[n]]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z H'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
ksuuiml9iiuql87hdjafzwxi4nznqo7
773002
772983
2026-09-24T07:06:56Z
Xhungab
23827
773002
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n x[n]|fond={{{fond|}}<nowiki>}</nowiki>}}
Si Z[x(n)] = X(z) alors '''Z[n x(n)] = -z X'(z)'''
'''Le signal''' '''Transformée en Z'''
'''x[n]''' '''X(z)''' '''Z[n x[n]] = -z X'(z)'''
'''u[n] = 1''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''r[n] = n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''c[n] = n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''f[n] = a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal g[n]''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n g[n]]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z G'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal h[n]''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n h[n]]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z H'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
5qjruw196bq83zbohzxl72z2h7a8hq0
Mathc initiation/a589
0
81072
772972
772707
2026-09-23T21:13:20Z
Xhungab
23827
772972
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Le retard de no unités : x(n-no)|fond={{{fond|}}<nowiki>}</nowiki>}}
x[n-no] est un signal causal retardé de no unités.
Image (L'invité retardé de no minutes, arrivera à 12h+no minutes)
Si Z[x[n]] = X(z) alors '''Z[x[n-no]] = z^(-no) X(z)'''
'''Le signal''' '''Transformée en Z '''
'''x(n)''' '''X(z)''' '''Z[x(n-no)] = z^(-no) X(z)'''
'''u(n)''' '''z/(z-1)''' '''z^(-no) z/(z-1)'''
'''n''' '''z/(z-1)^2''' '''z^(-no) z/(z-1)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(-no) z(z+1)/(z-1)^3'''
'''a^n''' '''z/(z-a)''' '''z^(-no) z/(z-a)'''
'''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
bf54gwoohq10ifj1i1cs9czu5ztg08e
772981
772972
2026-09-23T21:26:02Z
Xhungab
23827
772981
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|Le retard de no unités : x(n-no)|fond={{{fond|}}<nowiki>}</nowiki>}}
x[n-no] est un signal causal retardé de no unités.
Image (L'invité retardé de no minutes, arrivera à 12h+no minutes)
Si Z[x[n]] = X(z) alors '''Z[x[n-no]] = z^(-no) X(z)'''
'''Le signal''' '''Transformée en Z '''
'''x(n)''' '''X(z)''' '''Z[x(n-no)] = z^(-no) X(z)'''
'''u(n)''' '''z/(z-1)''' '''z^(-no) z/(z-1)'''
'''n''' '''z/(z-1)^2''' '''z^(-no) z/(z-1)^2'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(-no) z(z+1)/(z-1)^3'''
'''a^n''' '''z/(z-a)''' '''z^(-no) z/(z-a)'''
'''cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(-no) [z sin(k)]/[z^2-2z cos(k)+1]'''
{{AutoCat}}
depcyuv4zp8tx5vrkni6tzdgcz9dit1
Mathc initiation/a590
0
81073
772986
772708
2026-09-23T21:38:26Z
Xhungab
23827
772986
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|L'avance d'une unité : x(n+1)|fond={{{fond|}}<nowiki>}</nowiki>}}
'''Le signal ''' '''Transformée en Z''' '''Z(x[n+1])'''
'''x[n]''' '''X(z)''' '''z^(1) [ X(z) - x(0) ]'''
'''u[n]''' '''z/(z-1)''' '''z^(1) [ z/(z-1) - u(0) ]'''
'''n''' '''z/(z-1)^2''' '''z^(1) [ z/(z-1)^2 - 0 ]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(1) [ z(z+1)/(z-1)^3 - 0^2 ]'''
'''a^n''' '''z/(z-a)''' '''z^(1) [ z/(z-a) - a^0 ]'''
'''cos[kn]''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [z^2-z cos(k)]/(z^2-2z cos(k)+1) - cos(k0)]'''
'''sin[kn]''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [ z sin(k)/(z^2-2z cos(k)+1) - sin(k0)]'''
{{AutoCat}}
b99xbil8wj4bm6e8hd251xrxpcoyo2m
773003
772986
2026-09-24T07:09:59Z
Xhungab
23827
773003
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|L'avance d'une unité : x(n+1)|fond={{{fond|}}<nowiki>}</nowiki>}}
x(n+1) est un signal causal avancé de 1 unité.
Image (L'invité en avance de 1 minute, arrivera à 12h-1 minute, 11h59 minutes)
Si Z[x(n)] = X(z) alors '''Z[x(n+1)] = z^(1) [X(z)-x(0)]'''
'''Le signal ''' '''Transformée en Z''' '''Z(x[n+1])'''
'''x[n]''' '''X(z)''' '''z^(1) [ X(z) - x(0) ]'''
'''u[n]''' '''z/(z-1)''' '''z^(1) [ z/(z-1) - u(0) ]'''
'''n''' '''z/(z-1)^2''' '''z^(1) [ z/(z-1)^2 - 0 ]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^(1) [ z(z+1)/(z-1)^3 - 0^2 ]'''
'''a^n''' '''z/(z-a)''' '''z^(1) [ z/(z-a) - a^0 ]'''
'''cos[kn]''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [z^2-z cos(k)]/(z^2-2z cos(k)+1) - cos(k0)]'''
'''sin[kn]''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' '''z^(1) [ z sin(k)/(z^2-2z cos(k)+1) - sin(k0)]'''
Remarque :
Lors d'une avance, certains éléments de la suite x(n) peuvent passer du côté négatif.
Dans ce cas, il suffit d'ignorer ces éléments :
-1 0 1 2 3 4
x(0) x(1) x(2) x(3) x(4)
<- x(0) x(1) x(2) x(3) x(4) x(5) L'avance d'une unité.
x(1) x(2) x(3) x(4) x(5)
{{AutoCat}}
10hbqpg7evyur8wn2g5slmg5ggh7pa2
Mathc initiation/a591
0
81074
772988
772709
2026-09-23T21:44:51Z
Xhungab
23827
772988
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|L'avance de deux unités : x(n+2)|fond={{{fond|}}<nowiki>}</nowiki>}}
'''Le signal''' '''Transformée en Z ''' '''Z[x[n+2])'''
'''x[n]''' '''X(z)''' '''z^2 [ X(z) -x(0) -x(1) z^(-1)]'''
'''u[n]''' '''z/(z-1)''' '''z^2 [ z/(z-1) -u(0) -u(1) z^(-1)]'''
'''n''' '''z/(z-1)^2''' '''z^2 [ z/(z-1)^2 -0 -1 z^(-1)]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^2 [z(z+1)/(z-1)^3 -0^2 -1^2 z^(-1)]'''
'''a^n''' '''z/(z-a)''' '''z^2 [ z/(z-a) -a^0 -a^1 z^(-1)]'''
'''Le signal g[n]''' : '''cos(kn)'''
'''Transformée en Z ''' : '''(z^2-z cos(k))/(z^2-2z cos(k)+1)'''
'''Z(g[n+2])''' : '''z^2 (z^2-z cos(k))/(z^2-2z cos(k)+1) -cos(k0) -cos(k1) z^(-1)]'''
'''Le signal h[n]''' : '''sin(kn)'''
'''Transformée en Z ''' : '''z sin(k)/(z^2-2z cos(k)+1)'''
'''Z(h[n+2])''' : '''z^2 z sin(k)/(z^2-2z cos(k)+1) -sin(k0) -sin(k1) z^(-1)]'''
{{AutoCat}}
1ieil1i08oxs3aexzn5t5ymfd0ws95z
773004
772988
2026-09-24T07:14:17Z
Xhungab
23827
773004
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
{{Partie{{{type|}}}|L'avance de deux unités : x(n+2)|fond={{{fond|}}<nowiki>}</nowiki>}}
x(n+2) est un signal causal avancé de 2 unité.
Image (L'invité en avance de 2 minutes, arrivera à 12h-2 minutes, 11h58 minutes)
Si Z[x(n)] = X(z) alors '''Z[x(n+2)] = z^(2) [X(z)-x(0)-x(1)z^(-1)]'''
'''Le signal''' '''Transformée en Z ''' '''Z[x[n+2])'''
'''x[n]''' '''X(z)''' '''z^2 [ X(z) -x(0) -x(1) z^(-1)]'''
'''u[n]''' '''z/(z-1)''' '''z^2 [ z/(z-1) -u(0) -u(1) z^(-1)]'''
'''n''' '''z/(z-1)^2''' '''z^2 [ z/(z-1)^2 -0 -1 z^(-1)]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''z^2 [z(z+1)/(z-1)^3 -0^2 -1^2 z^(-1)]'''
'''a^n''' '''z/(z-a)''' '''z^2 [ z/(z-a) -a^0 -a^1 z^(-1)]'''
'''Le signal g[n]''' : '''cos(kn)'''
'''Transformée en Z ''' : '''(z^2-z cos(k))/(z^2-2z cos(k)+1)'''
'''Z(g[n+2])''' : '''z^2 (z^2-z cos(k))/(z^2-2z cos(k)+1) -cos(k0) -cos(k1) z^(-1)]'''
'''Le signal h[n]''' : '''sin(kn)'''
'''Transformée en Z ''' : '''z sin(k)/(z^2-2z cos(k)+1)'''
'''Z(h[n+2])''' : '''z^2 z sin(k)/(z^2-2z cos(k)+1) -sin(k0) -sin(k1) z^(-1)]'''
Remarque :
Lors d'une avance, certains éléments de la suite x(n) peuvent passer du côté négatif.
Dans ce cas, il suffit d'ignorer ces éléments :
-2 -1 0 1 2 3 4
x(0) x(1) x(2) x(3) x(4)
<- x(0) x(1) x(2) x(3) x(4) x(5) x(6) L'avance de deux unités.
x(1) x(2) x(3) x(4) x(5)
{{AutoCat}}
5sji1bxvxilgy6jnk2wj5jz2p0fa32e
Mathc initiation/0070
0
84515
773008
772654
2026-09-24T08:56:06Z
Xhungab
23827
773008
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
'''La transformée en Z de u(n) est : U(z) = z/(z-1)'''
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) x = z^(-1)
= 1/(1-1/z) z^(-1) = 1/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
(*) sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = (*) Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) = n n = n^2 alors Z[x(n)] = Z[n r(n)] = -z R'(z)
R'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) R'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
sum (a 1/z )^(n), n=0 to infinity z^(-1) = 1/z
sum (a/z)^(n), n=0 to infinity
Remarque * sum x^n, n=0 to infinity = 1/(1-x) |x|<1
* sum (a/z)^(n), n=0 to infinity = 1/(1-(a/z)) Remplaçons x par (a/z)
= 1/(1-(a/z)) Multiplions par z/z
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
bcqp1xsh566q478us28fb8owphwj3tw
773009
773008
2026-09-24T08:57:12Z
Xhungab
23827
773009
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
= 1/(1-1/z) Multiplions par z/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
(*) sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = (*) Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) = n n = n^2 alors Z[x(n)] = Z[n r(n)] = -z R'(z)
R'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) R'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
sum (a 1/z )^(n), n=0 to infinity z^(-1) = 1/z
sum (a/z)^(n), n=0 to infinity
Remarque * sum x^n, n=0 to infinity = 1/(1-x) |x|<1
* sum (a/z)^(n), n=0 to infinity = 1/(1-(a/z)) Remplaçons x par (a/z)
= 1/(1-(a/z)) Multiplions par z/z
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
qn313y38zkii9lj1xomonvmfrjwsx1p
Mathc initiation/0072
0
84518
772937
772666
2026-09-23T17:56:06Z
Xhungab
23827
772937
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==La fonction sinus discret : h(n) = sin(kn)==
'''La transformée en Z de h[n] est :'''
H[z] = z sin(k)]/[z^2-2z cos(k)+1
h[n] = sin(kn) * sin(kn) = (e^(kni)-e^(-kni))/2i
h[n] = (e^(kni)-e^(-kni))/2i
h[n] = 1/2i ('''e^(kni)'''-e^(-kni))
f(n) = a^n F[z] = z/(z-a)
H[z] = 1/2i (z/(z-e^(ki)) - z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))'''
H[z] = z/2i (1/(z-e^(ki)) - 1/(z-e^(-ki))
Mis au même dénominateur.
H[z] = z/2i ((z-e^(-ki) - (z-e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
H[z] = z/2i ( z-e^(-ki) - z +e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
H[z] = z/2i ((-e^(-ki) +e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
'''Numérateur :'''
H[z] = z/2i ((-e^(-ki)+e^(ki)) / ... * e^(ki)-e^(-ki) = 2i sin(k)
H[z] = z/2i (2i sin(k)) /...
H[z] = '''z sin(k)''' /...
'''Dénominateur :'''
H[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons :
H[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''')
H[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1'''
H[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) '''e^(ki)+e^(-ki) = 2 cos(k)'''
H[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1)
H[z] = ... / ('''z^2 - 2 z cos(k) + 1)'''
'''Donc :'''
H[z] = z sin(k) / (z^2 - 2 z cos(k) + 1)
{{AutoCat}}
0ag39fzf7p3dj4023b4jur5f3acicue
773007
772937
2026-09-24T08:49:31Z
Xhungab
23827
773007
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==La fonction sinus discret : h(n) = sin(kn)==
'''La transformée en Z de h[n] est :'''
H[z] = z sin(k)]/[z^2-2z cos(k)+1]
h[n] = sin(kn) * sin(kn) = (e^(kni)-e^(-kni))/2i
h[n] = (e^(kni)-e^(-kni))/2i
h[n] = 1/2i ('''e^(kni)'''-e^(-kni))
f(n) = a^n F[z] = z/(z-a)
H[z] = 1/2i ('''z/(z-e^(ki))''' - z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))'''
H[z] = '''z'''/2i (1/(z-e^(ki)) - 1/(z-e^(-ki))
Mis au même dénominateur.
H[z] = z/2i ((z-e^(-ki) - (z-e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
H[z] = z/2i (( -e^(-ki) +e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
'''Numérateur :'''
H[z] = z/2i (('''-e^(-ki)+e^(ki)''') / ... * '''e^(ki)-e^(-ki)''' = 2i sin(k)
H[z] = z/2i (2i sin(k)) /...
H[z] = '''z sin(k)''' /...
'''Dénominateur :'''
H[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons :
* '''e^(ki)(e^(-ki) = 1'''
H[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''')
H[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1)
H[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)'''
H[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1)
H[z] = ... / ('''z^2 - 2 z cos(k) + 1)'''
'''Donc :'''
H[z] = z sin(k) / (z^2 - 2 z cos(k) + 1)
{{AutoCat}}
2g4qum1c9qxr6o9rd5ik4veh8aelqwe
Mathc initiation/0073
0
84519
772992
772820
2026-09-23T21:50:06Z
Xhungab
23827
772992
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|La transformée en Z : Quelques astuces}}
La fonction δ[n] est l'impulsion de Kronecker, qui vaut 1 pour n = 0 et 0 ailleurs.
Pour un signal causal on utilise :
* z <--> δ[n+1] * 1 <--> δ[n]
* z^2 <--> δ[n+2] * 5 <--> 5 δ[n]
* z^k <--> δ[n+k] * C <--> C δ[n]
Le cas : 1/(z-a)
Pour résoudre le problème, il faut introduire un "z" au numérateur. Les transformées en "Z" ont, en générales un "z" au numérateur.
On peut utiliser :
1/(z-a) = 1/a (z/(z-a) - 1)
Vérifions :
1 z 1 z (z-a) 1 z - z+a 1 a 1
- (--- - 1) = - (--- - -----) = - (-------) = - (---) = ---
a z-a a z-a z-a a z-a a z-a z-a
z^2+4z+3
Soit : --------
z^2-4z-3
Effectuez l'opération :
z^2-4z-3
________ Quotient = 1
z^2+4z+3 | 1 Reste = 8z+6
- (z^2-4z-3)
----------
0+8z+6
z^2+4z+3 8z+6
Soit : -------- = 1 + --------
z^2-4z-3 z^2-4z-3
{{AutoCat}}
qnus2y02jj4arx78izsw4pxp37v036c
Mathc initiation/0075
0
84520
772961
2026-09-23T20:45:09Z
Xhungab
23827
news
772961
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|la Transformée en Z : Quelques applications}}
{{Partie{{{type|}}}|[[Mathc initiation/0074|* Résoudre : y(n) - 2 y(n-1) = δ(n) ]]}}
'''Quelques rappels mathématiques : '''
{{Partie{{{type|}}}|[[Mathc initiation/a566|* Trigonométriques. Trigonométriques hyperboliques]]}}
{{Partie{{{type|}}}|[[Mathc initiation/a569|* Fractions partielles]]}}
{{AutoCat}}
jjy8fl3i0kwbgxtn2klseohkerpq2kf
772976
772961
2026-09-23T21:16:24Z
Xhungab
23827
772976
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec la transformée en Z|Sommaire]]
{{Partie{{{type|}}}|la Transformée en Z : Quelques applications}}
{{Partie{{{type|}}}|[[Mathc initiation/0074|* Résoudre : y(n) - 2 y(n-1) = δ(n) ]]}}
'''Quelques rappels mathématiques : '''
{{Partie{{{type|}}}|[[Mathc initiation/a569|* Fractions partielles]]}}
{{AutoCat}}
kkq60caaqxpw358y60g7p3omjlllf6x
Mathc initiation/0074
0
84521
772967
2026-09-23T21:03:06Z
Xhungab
23827
news
772967
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
'''Résoudre l'équation aux différences :'''
y[n] - 2 y[n-1] = x[n]
avec x[n] = δ[n]
et une valeur initiale y[-1] = 0 (n = 0; y[n-1] = y[0-1] = y[-1])
on veut retrouver y[n]
'''Utilisons la transformée en z''' :
x[n] <--> X(z)
y[n] - 2 y[n-1] = δ[n] x[n-1] <--> z^(-1) X(z)
Le retard de no unités '''x[n-no] <--> z^(-no) X(z)'''
Y(z) - 2 {'''z^(-1) Y(z)'''} = 1 δ[n] <--> 1
Y(z)(1-2 z^(-1)) = 1 Isolons Y(z)
Y(z) = 1/(1-2 z^(-1))
Y(z) = '''z/(z-2)''' Multiplions le numérateur et le dénominateur par z
Soit : y(n) = '''2^(n) u[n]''' '''z/(z-a) <--> a^n u[n]'''
{{AutoCat}}
4k3030bkpvwrqqzvd21s000l7uifooa
772974
772967
2026-09-23T21:15:28Z
Xhungab
23827
772974
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
'''Résoudre l'équation aux différences :'''
y[n] - 2 y[n-1] = x[n]
avec x[n] = δ[n]
et une valeur initiale y[-1] = 0 (n = 0; y[n-1] = y[0-1] = y[-1])
on veut retrouver y[n]
'''Utilisons la transformée en z''' :
x[n] <--> X(z)
y[n] - 2 y[n-1] = δ[n] x[n-1] <--> z^(-1) X(z)
[[Mathc initiation/a589|Le retard de no unités]] '''x[n-no] <--> z^(-no) X(z)'''
Y(z) - 2 {'''z^(-1) Y(z)'''} = 1 δ[n] <--> 1
Y(z)(1-2 z^(-1)) = 1 Isolons Y(z)
Y(z) = 1/(1-2 z^(-1))
Y(z) = '''z/(z-2)''' Multiplions le numérateur et le dénominateur par z
Soit : y(n) = '''2^(n) u[n]''' '''z/(z-a) <--> a^n u[n]'''
{{AutoCat}}
sxcfqyd580p5ka0198r8h92cxipxlbc
772979
772974
2026-09-23T21:23:37Z
Xhungab
23827
772979
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/0075|Sommaire]]
'''Résoudre l'équation aux différences :'''
y[n] - 2 y[n-1] = x[n]
avec x[n] = δ[n]
et une valeur initiale y[-1] = 0 (n = 0; y[n-1] = y[0-1] = y[-1])
on veut retrouver y[n]
'''Utilisons la transformée en z''' :
x[n] <--> X(z)
y[n] - 2 y[n-1] = δ[n] x[n-1] <--> z^(-1) X(z)
[[Mathc initiation/a589|Le retard de no unités]] '''x[n-no] <--> z^(-no) X(z)'''
Y(z) - 2 {'''z^(-1) Y(z)'''} = 1 δ[n] <--> 1
Y(z)(1-2 z^(-1)) = 1 Isolons Y(z)
Y(z) = 1/(1-2 z^(-1))
Y(z) = '''z/(z-2)''' Multiplions le numérateur et le dénominateur par z
Soit : y(n) = '''2^(n) u[n]''' '''z/(z-a) <--> [[Mathc initiation/a585|a^n u[n]]]'''
{{AutoCat}}
1zdclz6zcks6vu1h4h35y2ur8feja91